Compare commits

...
Author SHA1 Message Date
58ff4ad3a9 Python: Hyperlight: thread-confine sandbox, skip parsing on host callbacks, schema/tool cleanup (#5424)
* improved parsing of tool call results and tweaks

* Address PR review: skip_parsing flag, broader registry close, comment fix

- FunctionTool.invoke now takes a boolean skip_parsing flag instead of the
  SKIP_PARSING sentinel; the sentinel is still accepted as result_parser at
  construction time to opt out of parsing for every call. The two paths are
  equivalent.
- _SandboxRegistry.close now invokes any sandbox close/shutdown hook on the
  entry's own worker thread (PyO3 unsendable), then shuts the worker down,
  then cleans up the per-entry temporary directories.
- Clarified the _SandboxWorker.shutdown comment to describe the actual
  ThreadPoolExecutor.shutdown(wait=False, cancel_futures=False) semantics.
- Hyperlight host callback uses skip_parsing=True (the new flag).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Drop redundant 'is not SKIP_PARSING' guard that mypy 1.x flags

After callable(configured_parser) the sentinel is already excluded; the extra
identity check tripped mypy's non-overlapping identity warning.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fixed sandbox working on copy of tool

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-23 08:14:51 +00:00
66e02c10e3 .NET: [Breaking] Migrate A2A agent and hosting to A2A SDK v1 (#5423)
* update a2a agent to the latest a2a sdk (#5257)

* Move A2A samples from 04-hosting to 02-agents (#5267)

Move the A2A sample projects (A2AAgent_AsFunctionTools and
A2AAgent_PollingForTaskCompletion) from samples/04-hosting/A2A/ to
samples/02-agents/A2A/ to better align with the sample directory
structure. Update solution file and samples README accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* .NET: Fix stream reconnection for A2AAgent (#5275)

* Add SSE stream reconnection support to A2AAgent

Implement automatic reconnection for SSE streams that disconnect mid-task,
using the Last-Event-ID header to resume from where the stream left off.

Changes:
- Add InvokeStreamingWithReconnectAsync method to A2AAgent with configurable
  max retries and delay between attempts
- Add new log messages for reconnection events
- Add A2AAgent_StreamReconnection sample demonstrating the feature
- Update existing polling sample to use simplified SendMessageAsync API
- Add unit tests for stream reconnection logic

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* address comments

* Address PR review feedback

- Dispose SSE enumerator before GetTaskAsync fallback to release HTTP connection
- Wrap StreamWriter in using blocks with leaveOpen:true and explicit UTF-8 encoding
- Print update.Text instead of update object in stream reconnection sample

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* .NET: Use IA2AClientFactory to create A2AClient (#5277)

* Refactor A2A extensions to use IA2AClientFactory and add ProtocolSelection sample

- Update A2AAgentCardExtensions to accept IA2AClientFactory instead of A2AClientOptions
- Update A2ACardResolverExtensions to accept IA2AClientFactory
- Update A2AClientExtensions to accept IA2AClientFactory
- Update A2AAgent to use IA2AClientFactory for client creation
- Add A2AAgent_ProtocolSelection sample demonstrating protocol selection
- Add comprehensive unit tests for all changes
- Update README files with new sample reference

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Reorder params: options before loggerFactory in A2A extensions

Move A2AClientOptions parameter before ILoggerFactory in AsAIAgent
and GetAIAgentAsync extension methods to follow the repo convention
of keeping LoggerFactory and CancellationToken as the last parameters.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* .NET: Migrate A2A hosting to A2A SDK v1 (#5363)

* .NET: Migrate A2A hosting to A2A SDK v1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* remove unused agent card

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* .NET: Split A2A endpoint mapping into protocol-specific methods (#5413)

* .NET: Refactor A2A hosting registration into A2AServerServiceCollectionExtensions

- Rename A2AHostingOptions to A2AServerRegistrationOptions
- Move server registration logic from A2AEndpointRouteBuilderExtensions
  and AIAgentExtensions into new A2AServerServiceCollectionExtensions
- Remove A2AProtocolBinding and AIAgentExtensions (consolidated)
- Update samples and tests to use the new registration API

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* address copilot comments

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove unnecessary using directive in AgentWebChat.AgentHost

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* restore AsyncEnumerable package version

* address copilot initial feedback

* address automated code review and formatting issues

* fix formatting issues

* Add DI wiring verification tests for AddA2AServer

Add three tests to A2AServerServiceCollectionExtensionsTests that verify
custom keyed services are actually wired through to the A2AServer, not
just that the server resolves non-null:

- Custom IAgentHandler: verifies the keyed handler is invoked when
  processing a SendMessageRequest instead of the default A2AAgentHandler.
- Custom AgentSessionStore (no handler): verifies the keyed session
  store's GetSessionAsync is called during request processing when no
  custom handler is registered.
- Default stores end-to-end: verifies the InMemoryAgentSessionStore and
  InMemoryTaskStore defaults successfully process a request. Uses a new
  CreateAgentMockForRequests helper that includes SerializeSessionCoreAsync
  setup needed by InMemoryAgentSessionStore.

All tests call A2AServer.SendMessageAsync directly (no HTTP layer needed)
and use CancellationToken timeouts to guard against hangs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-23 07:53:00 +00:00
Evan MattsonandGitHub acec9caa2f Python: Bump Python package versions for a release. (#5432)
* Bump Python version for a release.

* Revert lockstep bumps on unchanged connectors

Per PR review: only connectors that changed (or whose published metadata
changed) should get new versions. Keeps released tier at 1.1.1, a2a/ag-ui
at 1.0.0b260422, foundry-hosting at 1.0.0a260422; reverts the 19 unchanged
betas and 2 unchanged alphas to 1.0.0b260421/1.0.0a260421. Reverts all 26
non-core agent-framework-core floors to >=1.1.0,<2 since no connector
actually depends on a 1.1.1 API or bug fix.

* Restore lockstep prerelease bumps and raise core floors to >=1.1.1

Reverses the lean-revert: all beta packages stamped 1.0.0b260423 and alpha
packages stamped 1.0.0a260423 (Asia date, matching release cut time). All
26 non-core packages raise agent-framework-core lower bound from >=1.1.0,<2
to >=1.1.1,<2 to signal the validated cohort for this release. CHANGELOG
date updated to 2026-04-23.
2026-04-23 16:40:14 +09:00
Evan MattsonandGitHub 5d4873888f Don't fail if review issue occurs (#5434) 2026-04-23 13:24:21 +09:00
Evan MattsonandGitHub e2f161c8a0 Pin to specific release (#5430) 2026-04-23 08:23:56 +09:00
3f23e1dfbf Python: Flaky test report (#5342)
* Add flaky test trend reporting to CI workflows

Parse JUnit XML (pytest.xml) from each integration test job and
aggregate results into a markdown trend report showing per-test
pass/fail/skip status across the last 5 runs.

Changes:
- Add python/scripts/flaky_report/ package (JUnit XML parser + trend
  report generator following the sample_validation pattern)
- Add upload-artifact steps to all 6 integration test jobs in both
  python-merge-tests.yml and python-integration-tests.yml
- Add python-flaky-test-report aggregation job with history caching
- Add --junitxml=pytest.xml to integration-tests.yml jobs (already
  present in merge-tests.yml)
- Fix Cosmos job --junitxml path (use absolute path since uv run
  --directory changes cwd)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix flaky report: handle missing test results gracefully

- Guard against missing reports directory in load_current_run()
- Only run report job when at least one integration test job completed
  (skip when all jobs are skipped, e.g. on pull_request events)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review: fix provider names and if-expression precedence

- Use explicit provider name mapping in _derive_provider() so OpenAI
  renders correctly instead of 'Openai'
- Fix operator precedence in workflow if-expressions by wrapping
  success/failure checks in parentheses

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add File column and xfail detection to flaky test report

- Add File column showing module name (e.g., test_openai_chat_client)
  to disambiguate tests with the same function name across files
- Detect pytest xfail tests in JUnit XML (type=pytest.xfail) and
  show them with a distinct warning emoji instead of skip emoji
- Update legend to include xfail explanation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add Foundry embedding env vars to merge-tests workflow

Sync the Foundry integration job in python-merge-tests.yml with
python-integration-tests.yml by adding FOUNDRY_MODELS_ENDPOINT,
FOUNDRY_MODELS_API_KEY, FOUNDRY_EMBEDDING_MODEL, and
FOUNDRY_IMAGE_EMBEDDING_MODEL. Once the repo variables/secrets
are configured, the embedding integration test will run in CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix File column showing class name instead of module name

When a test is inside a class, pytest writes the classname as e.g.
'pkg.test_file.TestClass'. The previous rsplit logic extracted
'TestClass' instead of 'test_file'. Now detect uppercase-starting
segments as class names and use the preceding segment instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review: UTC timestamps, XML error handling, summary fix, docstring

- Use datetime.now(timezone.utc) for accurate UTC timestamps
- Catch ET.ParseError per-file so corrupt XML doesn't crash the report
- Remove separate 'error' key from summary (errors folded into 'failed')
- Fix _short_name docstring to show actual dotted classname::name format

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 20:16:50 +00:00
Evan MattsonandGitHub d75f874d78 Adjust dev status for alpha from 4 to 3 in foundry hosting pyproject.toml (#5387) 2026-04-22 17:48:02 +00:00
0b50455e75 Python: Pass client thread_id as session_id when constructing AgentSession in AG-UI (#5384)
* Pass thread_id as session_id when constructing AgentSession in AG-UI

run_agent_stream() was constructing AgentSession without passing the
client's thread_id as session_id, causing every request to receive a
random UUID. This broke session continuity for HistoryProvider
implementations that rely on session_id matching the client's thread_id.

Pass session_id=thread_id in both the service-session and non-service
code paths so the session identity is consistent with the AG-UI client.

Fixes #5357

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add test for service_session with no thread_id edge case (#5357)

When use_service_session=True but no thread_id/threadId is in the payload,
verify session_id is a generated UUID and service_session_id is None.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 17:45:25 +00:00
3ae86f098e Python: Propagate thread_id and forwarded_props through AG-UI to A2A context_id (#5383)
* Propagate session.service_session_id as A2A context_id

When A2AAgent is used behind the AG-UI protocol, the client thread_id is
stored in session.service_session_id but was never forwarded as the A2A
context_id. This broke session continuity across the AG-UI → A2A boundary.

Add an optional context_id keyword argument to _prepare_message_for_a2a()
and pass session.service_session_id from run(). The explicit
message.additional_properties["context_id"] still takes precedence.

Fixes #5345

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add integration tests for session context_id wiring in run() (#5345)

- Enhance MockA2AClient.send_message to capture last_message for assertions
- Add test_run_passes_session_service_session_id_as_context_id: verifies
  run() passes session.service_session_id through to A2A message context_id
- Add test_run_message_context_id_takes_precedence_over_session: verifies
  explicit message context_id wins over session fallback
- Update _prepare_message_for_a2a docstring to document context_id param
  and its precedence rules

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #5345: Python: [Bug]: Inconvenient passing of context_id / thread_id in A2A/AG-UI implementations

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 17:44:41 +00:00
Evan MattsonandGitHub fffd0acb3e Python: fix(foundry): reconcile toolbox hosted-tool payloads with Responses API (#5414)
* fix(foundry): reconcile toolbox hosted-tool payloads with Responses API

* docs(foundry): update create_sample_toolbox docstring to reflect all tools created
2026-04-22 17:43:26 +00:00
ea3320d39f Python: Fix OpenAI Responses streaming to propagate created_at from final response.completed event (#5382)
* Fix streaming response losing created_at from response.completed event (#5347)

The streaming path in _parse_chunk_from_openai did not extract created_at
from the response.completed event, unlike the non-streaming path in
_parse_responses_response. This caused durabletask persistence warnings
when created_at was None.

Extract created_at in the response.completed case and pass it to the
returned ChatResponseUpdate.

Also fix pre-existing pyright errors for optional orjson import in sample
files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix orjson import suppression to use pyright instead of mypy (#5347)

Replace `# type: ignore[import-not-found]` with
`# pyright: ignore[reportMissingImports]` on optional orjson imports
in conversation sample files, matching the repo's Pyright strict
configuration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 06:19:31 +00:00
9e915b36b6 Add pr review GH workflow (#5418)
* Add workflow PR review

* Allow reviews on draft PRs

* Update .github/workflows/devflow-pr-review.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update .github/workflows/devflow-pr-review.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Bump actions/checkout to v6 and uv to 0.11.x

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-22 13:52:42 +09:00
bca40a7e90 Python: fix: exclude null file_id from input_image payload to prevent 400 sch… (#5125)
* fix: exclude null file_id from input_image payload to prevent 400 schema error (#5120)

* test: add case for additional_properties present without file_id key

---------

Co-authored-by: Sergey Borisov <sergey.borisov@dataimpact.io>
2026-04-21 22:29:00 +00:00
f2b215a2f6 .NET [WIP] Foundry Hosted Agents Support (#5312)
* Add Azure AI Foundry Responses hosting adapter

Implement Microsoft.Agents.AI.Hosting.AzureAIResponses to host agent-framework
AIAgents and workflows within Azure Foundry as hosted agents via the
Azure.AI.AgentServer.Responses SDK.

- AgentFrameworkResponseHandler: bridges ResponseHandler to AIAgent execution
- InputConverter: converts Responses API inputs/history to MEAI ChatMessage
- OutputConverter: converts agent response updates to SSE event stream
- ServiceCollectionExtensions: DI registration helpers
- 336 unit tests across net8.0/net9.0/net10.0 (112 per TFM)
- ResponseStreamValidator: SSE protocol validation tool for samples
- FoundryResponsesHosting sample app

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Bump System.ClientModel to 1.10.0 for Azure.Core 1.52.0 compat

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clean up tests and sample formatting

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update Azure.AI.AgentServer packages to 1.0.0-alpha.20260401.5

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add hosted package version suffix (0.9.0-hosted) to distinguish from mainline

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Move Foundry Responses hosting into Microsoft.Agents.AI.Foundry package

Move source and test files from the standalone Hosting.AzureAIResponses project
into the Foundry package under a Hosting/ subfolder. This consolidates the
Foundry-specific hosting adapter into the main Foundry package.

- Source: Microsoft.Agents.AI.Foundry.Hosting namespace
- Tests: merged into Foundry.UnitTests/Hosting/
- Conditionally compiled for .NETCoreApp TFMs only (net8.0+)
- Deleted standalone Hosting.AzureAIResponses project and test project
- Updated sample and solution references

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Bump package version to 0.9.0-hosted.260402.2

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Bump OpenTelemetry packages to fix NU1109 downgrade errors

- OpenTelemetry/Api/Exporter.Console/Exporter.InMemory: 1.13.1 -> 1.15.0
- OpenTelemetry.Exporter.OpenTelemetryProtocol: already 1.15.0
- OpenTelemetry.Extensions.Hosting: already 1.14.0
- OpenTelemetry.Instrumentation.AspNetCore/Http: already 1.14.0
- OpenTelemetry.Instrumentation.Runtime: 1.13.0 -> 1.14.0
- Azure.Monitor.OpenTelemetry.Exporter: 1.4.0 -> 1.5.0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix CA1873: guard LogWarning with IsEnabled check

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix model override bug and add client REPL sample

- InputConverter: stop propagating request.Model to ChatOptions.ModelId
  Hosted agents use their own model; client-provided model values like
  'hosted-agent' were being passed through and causing server errors.
- Add FoundryResponsesRepl sample: interactive CLI client that connects
  to a Foundry Responses endpoint using ResponsesClient.AsAIAgent()
- Bump package version to 0.9.0-hosted.260403.1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Catch agent errors and emit response.failed with real error message

Previously, unhandled exceptions from agent execution would bubble up
to the SDK orchestrator, which emits a generic 'An internal server
error occurred.' message — hiding the actual cause (e.g., 401 auth
failures, model not found, etc.).

Now AgentFrameworkResponseHandler catches non-cancellation exceptions
and emits a proper response.failed event containing the real error
message, making it visible to clients and in logs.

OperationCanceledException still propagates for proper cancellation
handling by the SDK.

Also bumps package version to 0.9.0-hosted.260403.2.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Renaming and merging hosting extensions. (#5091)

* Rename AddAgentFrameworkHandler to AddFoundryResponses and add MapFoundryResponses

- Rename extension methods: AddAgentFrameworkHandler -> AddFoundryResponses, MapAgentFrameworkHandler -> MapFoundryResponses
- AddFoundryResponses now calls AddResponsesServer() internally
- Add MapFoundryResponses() extension on IEndpointRouteBuilder
- Update sample and tests to use new API names
- Remove redundant AddResponsesServer() and /ready endpoint from sample

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fixing numbering in sample.

---------

Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address breaking changes in 260408

* Bump hosted internal package version

* Add UserAgent middleware tests for Foundry hosting

* Hosting Samples update

* Hosting Samples update

* Hosting Samples update

* Hosting Samples update

* ChatClientAgent working

* Adding SessionStorage and SessionManagement, improving samples to align Consumption vs Hosting

* Using updates

* Update chat client agent for contributor and devs

* Foundry Agent Hosting

* Address text rag sample working

* Version bump

* Adding LocalTools + Workflow samples

* Removing extra using samples

* Add Hosted-McpTools sample with dual MCP pattern

Demonstrates two MCP integration layers in a single hosted agent:
- Client-side MCP: McpClient connects to Microsoft Learn, agent handles
  tool invocations locally (docs_search, code_sample_search, docs_fetch)
- Server-side MCP: HostedMcpServerTool delegates tool discovery and
  invocation to the LLM provider (Responses API), no local connection

Includes DevTemporaryTokenCredential for Docker local debugging,
Dockerfile.contributor for ProjectReference builds, and the openai/v1
route mapping for AIProjectClient compatibility in Development mode.

* .NET: Bump Azure.AI.AgentServer packages to 1.0.0-beta.1/beta.21 and fix br… (#5287)

* Bump Azure.AI.AgentServer packages to 1.0.0-beta.1/beta.21 and fix breaking API changes

- Azure.AI.AgentServer.Core: 1.0.0-beta.11 -> 1.0.0-beta.21
- Azure.AI.AgentServer.Invocations: 1.0.0-alpha.20260408.4 -> 1.0.0-beta.1
- Azure.AI.AgentServer.Responses: 1.0.0-alpha.20260408.4 -> 1.0.0-beta.1
- Azure.Identity: 1.20.0 -> 1.21.0 (transitive requirement)
- Azure.Core: 1.52.0 -> 1.53.0 (transitive requirement)
- Remove azure-sdk-for-net dev feed (packages now on nuget.org)
- Fix OutputConverter for new builder API (auto-tracked children, split EmitTextDone/EmitDone)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fixing small issues.

---------

Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add Azure AI Foundry Responses hosting adapter

Implement Microsoft.Agents.AI.Hosting.AzureAIResponses to host agent-framework
AIAgents and workflows within Azure Foundry as hosted agents via the
Azure.AI.AgentServer.Responses SDK.

- AgentFrameworkResponseHandler: bridges ResponseHandler to AIAgent execution
- InputConverter: converts Responses API inputs/history to MEAI ChatMessage
- OutputConverter: converts agent response updates to SSE event stream
- ServiceCollectionExtensions: DI registration helpers
- 336 unit tests across net8.0/net9.0/net10.0 (112 per TFM)
- ResponseStreamValidator: SSE protocol validation tool for samples
- FoundryResponsesHosting sample app

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Bump System.ClientModel to 1.10.0 for Azure.Core 1.52.0 compat

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clean up tests and sample formatting

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update Azure.AI.AgentServer packages to 1.0.0-alpha.20260401.5

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add hosted package version suffix (0.9.0-hosted) to distinguish from mainline

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Move Foundry Responses hosting into Microsoft.Agents.AI.Foundry package

Move source and test files from the standalone Hosting.AzureAIResponses project
into the Foundry package under a Hosting/ subfolder. This consolidates the
Foundry-specific hosting adapter into the main Foundry package.

- Source: Microsoft.Agents.AI.Foundry.Hosting namespace
- Tests: merged into Foundry.UnitTests/Hosting/
- Conditionally compiled for .NETCoreApp TFMs only (net8.0+)
- Deleted standalone Hosting.AzureAIResponses project and test project
- Updated sample and solution references

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Bump package version to 0.9.0-hosted.260402.2

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Bump OpenTelemetry packages to fix NU1109 downgrade errors

- OpenTelemetry/Api/Exporter.Console/Exporter.InMemory: 1.13.1 -> 1.15.0
- OpenTelemetry.Exporter.OpenTelemetryProtocol: already 1.15.0
- OpenTelemetry.Extensions.Hosting: already 1.14.0
- OpenTelemetry.Instrumentation.AspNetCore/Http: already 1.14.0
- OpenTelemetry.Instrumentation.Runtime: 1.13.0 -> 1.14.0
- Azure.Monitor.OpenTelemetry.Exporter: 1.4.0 -> 1.5.0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix CA1873: guard LogWarning with IsEnabled check

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix model override bug and add client REPL sample

- InputConverter: stop propagating request.Model to ChatOptions.ModelId
  Hosted agents use their own model; client-provided model values like
  'hosted-agent' were being passed through and causing server errors.
- Add FoundryResponsesRepl sample: interactive CLI client that connects
  to a Foundry Responses endpoint using ResponsesClient.AsAIAgent()
- Bump package version to 0.9.0-hosted.260403.1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Catch agent errors and emit response.failed with real error message

Previously, unhandled exceptions from agent execution would bubble up
to the SDK orchestrator, which emits a generic 'An internal server
error occurred.' message — hiding the actual cause (e.g., 401 auth
failures, model not found, etc.).

Now AgentFrameworkResponseHandler catches non-cancellation exceptions
and emits a proper response.failed event containing the real error
message, making it visible to clients and in logs.

OperationCanceledException still propagates for proper cancellation
handling by the SDK.

Also bumps package version to 0.9.0-hosted.260403.2.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Renaming and merging hosting extensions. (#5091)

* Rename AddAgentFrameworkHandler to AddFoundryResponses and add MapFoundryResponses

- Rename extension methods: AddAgentFrameworkHandler -> AddFoundryResponses, MapAgentFrameworkHandler -> MapFoundryResponses
- AddFoundryResponses now calls AddResponsesServer() internally
- Add MapFoundryResponses() extension on IEndpointRouteBuilder
- Update sample and tests to use new API names
- Remove redundant AddResponsesServer() and /ready endpoint from sample

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fixing numbering in sample.

---------

Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address breaking changes in 260408

* Bump hosted internal package version

* Add UserAgent middleware tests for Foundry hosting

* Hosting Samples update

* Hosting Samples update

* Hosting Samples update

* Hosting Samples update

* ChatClientAgent working

* Adding SessionStorage and SessionManagement, improving samples to align Consumption vs Hosting

* Using updates

* Update chat client agent for contributor and devs

* Foundry Agent Hosting

* Address text rag sample working

* Version bump

* Adding LocalTools + Workflow samples

* Removing extra using samples

* Add Hosted-McpTools sample with dual MCP pattern

Demonstrates two MCP integration layers in a single hosted agent:
- Client-side MCP: McpClient connects to Microsoft Learn, agent handles
  tool invocations locally (docs_search, code_sample_search, docs_fetch)
- Server-side MCP: HostedMcpServerTool delegates tool discovery and
  invocation to the LLM provider (Responses API), no local connection

Includes DevTemporaryTokenCredential for Docker local debugging,
Dockerfile.contributor for ProjectReference builds, and the openai/v1
route mapping for AIProjectClient compatibility in Development mode.

* Bump Azure.AI.AgentServer packages to 1.0.0-beta.1/beta.21 and fix breaking API changes

- Azure.AI.AgentServer.Core: 1.0.0-beta.11 -> 1.0.0-beta.21
- Azure.AI.AgentServer.Invocations: 1.0.0-alpha.20260408.4 -> 1.0.0-beta.1
- Azure.AI.AgentServer.Responses: 1.0.0-alpha.20260408.4 -> 1.0.0-beta.1
- Azure.Identity: 1.20.0 -> 1.21.0 (transitive requirement)
- Azure.Core: 1.52.0 -> 1.53.0 (transitive requirement)
- Remove azure-sdk-for-net dev feed (packages now on nuget.org)
- Fix OutputConverter for new builder API (auto-tracked children, split EmitTextDone/EmitDone)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fixing small issues.

* Fix IDE0009: add 'this' qualification in DevTemporaryTokenCredential

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix IDE0009: add 'this' qualification in all HostedAgentsV2 samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix CHARSET: add UTF-8 BOM to Hosted-LocalTools and Hosted-Workflows

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix dotnet format: add Async suffix to test methods (IDE1006), fix encoding and style

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Register AgentSessionStore in test DI setups

Add InMemoryAgentSessionStore registration to all ServiceCollection
setups in AgentFrameworkResponseHandlerTests and WorkflowIntegrationTests.
This is needed after the AgentSessionStore infrastructure was introduced
in the responses-hosting feature. Tests still have NotImplementedException
stubs for CreateSessionCoreAsync which will be fixed when the session
infrastructure is fully available.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add Invocations protocol samples (hosted echo agent + client) (#5278)

Add Hosted-Invocations-EchoAgent: a minimal echo agent hosted via the
Invocations protocol (POST /invocations) using AddInvocationsServer and
MapInvocationsServer, bridged to an Agent Framework AIAgent through a
custom InvocationHandler.

Add SimpleInvocationsAgent: a console REPL client that wraps HttpClient
calls to the /invocations endpoint in a custom InvocationsAIAgent,
demonstrating programmatic consumption of the Invocations protocol.

Both samples default to port 8088 for consistency with other hosted
agent samples.

* Restructure FoundryHostedAgents samples into invocations/ and responses/

Align dotnet hosted agent samples with the Python side (PR #5281) by
reorganizing the directory structure:

- Remove HostedAgentsV1 entirely (old API pattern)
- Split HostedAgentsV2 into invocations/ and responses/ based on protocol
- Move Using-Samples accordingly (SimpleAgent to responses, SimpleInvocationsAgent to invocations)
- Update slnx with new project paths and add previously missing invocations projects
- Update README cd paths from HostedAgentsV2 to invocations or responses
- Rename .env.local to .env.example to match Python naming convention
- Fix format violations in newly included invocations projects

* Remove launchSettings, use .env for port configuration

- Delete all launchSettings.json files (port 8088 now comes from ASPNETCORE_URLS in .env)
- Add DotNetEnv to Hosted-Invocations-EchoAgent so it loads .env like the responses samples
- Create .env.example for EchoAgent with ASPNETCORE_URLS and ASPNETCORE_ENVIRONMENT
- Add AGENT_NAME to ChatClientAgent and FoundryAgent .env.example (required by those samples)
- Add AZURE_BEARER_TOKEN=DefaultAzureCredential to all .env.example files
- Update DevTemporaryTokenCredential in all 6 samples to treat the sentinel value
  as unavailable, allowing ChainedTokenCredential to fall through to DefaultAzureCredential
- Update EchoAgent README with Configuration section

* Use placeholder for AGENT_NAME in Hosted-FoundryAgent .env.example

* Move FoundryResponsesHosting to responses/Hosted-WorkflowHandoff, use GetResponsesClient

* Rename Hosted-Workflows to Hosted-Workflow-Simple, Hosted-WorkflowHandoff to Hosted-Workflow-Handoff

* Remove FoundryResponsesRepl and empty FoundryResponsesHosting directory

* Add Dockerfiles, README, agent yamls and bearer token support to Hosted-Workflow-Handoff

- Add Dockerfile and Dockerfile.contributor for Docker-based testing
- Add agent.yaml and agent.manifest.yaml with triage-workflow as primary agent
- Add README.md following sibling pattern, noting Azure OpenAI vs Foundry endpoint
- Add DevTemporaryTokenCredential and ChainedTokenCredential for Docker auth
- Register triage-workflow as non-keyed default so azd invoke works without model
- Update .env.example with AZURE_BEARER_TOKEN sentinel
- Add .gitignore to 04-hosting to suppress VS-generated launchSettings.json
- Fix docker run image name in Hosted-Workflow-Simple README

* Fix AgentFrameworkResponseHandlerTests: implement session methods in test mock agents

* .NET: Auto-instrument resolved AIAgents with OpenTelemetry for Foundry Hosted Agents (#5316)

* Auto-instrument resolved AIAgents with OpenTelemetry using Core ResponsesSourceName

* Add OTel telemetry capture tests for Foundry hosted agent handler

* Net: Prepare Foundry Preview Release (#5336)

* Prepare Foundry preview release 1.2.0-preview.*

Bump VersionPrefix to 1.2.0 and update the preview stamp date. Invert packaging opt-in so only the Foundry preview set produces NuGet packages:

- Microsoft.Agents.AI.Abstractions

- Microsoft.Agents.AI

- Microsoft.Agents.AI.Workflows

- Microsoft.Agents.AI.Workflows.Generators

- Microsoft.Agents.AI.Foundry

Flip IsReleased=false on the preview set so they pick up the -preview.YYMMDD.N suffix. Gate GeneratePackageOnBuild on IsPackable=true. Remove the global IsPackable=true from nuget-package.props so the repo-level default (false) applies to everything else.

* Lower preview VersionPrefix to 0.0.1

Retroactive preview publish: bump VersionPrefix and GitTag from 1.2.0 to 0.0.1 so the 5 Foundry preview packages emit as 0.0.1-preview.260417.1.

* Net: Publish all packages as 0.0.1-preview.260417.2 (#5341)

Revises the Foundry pre-release approach to publish ALL normally packable src projects as preview packages stamped 0.0.1-preview.260417.2, including projects previously flagged IsReleased=true or with a non-default VersionSuffix (rc/alpha).

nuget-package.props:

- Collapse the four conditional PackageVersion expressions (IsReleaseCandidate, VersionSuffix, default preview, IsReleased stable) into a single unconditional 0.0.1-preview.260417.2. On this preview-only branch every package ships with the same pre-release stamp regardless of per-project flags.

- Restore the global IsPackable=true default (offsetting the repo-wide IsPackable=false in Directory.Build.props). Projects that opt out (Mem0, Declarative) already set IsPackable=false AFTER importing this file so they remain non-packable.

- Remove the IsReleased-gated EnablePackageValidation line. Package validation does not apply to a 0.0.1 preview.

csproj reverts (Abstractions, Agents.AI, Workflows, Workflows.Generators, Foundry):

- Revert the IsPackable=true opt-in block introduced in #5336 (now redundant since the props default is true again).

- Restore IsReleased=true to its pre-PR value. The setting is now a no-op because the props no longer branches on it.

* Bump preview version to 260420.1 and fix AgentServer package deps (#5367)

- Bump PackageVersion to 0.0.1-preview.260420.1
- Bump Azure.AI.AgentServer.Core beta.21 -> beta.22 (required by
  Azure.AI.AgentServer.Responses beta.3)
- Replace AgentHostTelemetry.ResponsesSourceName with local constant
  (type made internal in AgentServer.Core beta.22)

Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* .NET: Hosted agents toolbox support (#5368)

* feat: Add Foundry Toolbox (MCP) support to AgentFrameworkResponseHandler

Adds support for Foundry Toolsets MCP proxy integration in the hosted agent
response handler. Toolsets connect at startup via IHostedService, gating the
readiness probe per spec §3.1. MCP tools are injected into every request's
ChatOptions and OAuth consent errors (-32006) are intercepted and surfaced as
mcp_approval_request + incomplete SSE events.

New files:
- FoundryToolboxOptions.cs: configuration POCO for toolset names and API version
- FoundryToolboxBearerTokenHandler.cs: DelegatingHandler with Azure Bearer token
  auth, Foundry-Features header injection, and 3x exponential backoff on 429/5xx
- McpConsentContext.cs: AsyncLocal-based per-request consent state shared between
  the tool wrapper and the response handler
- ConsentAwareMcpClientTool.cs: AIFunction wrapper that catches -32006 errors and
  signals consent via shared state and linked CancellationTokenSource
- FoundryToolboxService.cs: IHostedService that creates McpClient per toolset at
  startup and exposes cached tools

Modified files:
- AgentFrameworkResponseHandler.cs: injects toolbox tools into ChatOptions, sets
  up linked CTS consent interception, emits mcp_approval_request on -32006
- ServiceCollectionExtensions.cs: adds AddFoundryToolboxes(params string[]) extension
- Microsoft.Agents.AI.Foundry.csproj: adds ModelContextProtocol and Azure.Identity
  dependencies under NETCoreApp condition

Sample:
- Hosted-Toolbox: minimal hosted agent sample using AddFoundryToolboxes

* Rename toolset to toolbox in user-facing API; rename ConsentAwareMcpClientTool to ConsentAwareMcpClientAIFunction

* Add HostedMcpToolboxAITool for client-selectable Foundry toolboxes

Introduces HostedMcpToolboxAITool, a marker tool subclassing HostedMcpServerTool that rides the OpenAI Responses 'mcp' wire format to let clients request a specific Foundry toolbox per request.

- New FoundryAITool.CreateHostedMcpToolbox(name, version?) factory.

- FoundryToolboxOptions.StrictMode (default true) rejects unregistered toolboxes; set to false to allow lazy-open on first use.

- FoundryToolboxService.GetToolboxToolsAsync(name, version?) resolves cached or lazy-opened MCP tools.

- AgentFrameworkResponseHandler parses request.Tools for foundry-toolbox://name[?version=v] markers and injects resolved tools per request, merging with pre-registered ones.

- Unit tests for marker parsing and strict-mode resolution.

* Bump Azure.AI.Projects to 2.1.0-alpha; add ToolboxRecord/ToolboxVersion factory overloads + tests

* Fix PR review issues: retry off-by-one, URI encoding, docs, tests, build

- Fix off-by-one in FoundryToolboxBearerTokenHandler retry loop (4 attempts → 3)
- URI-encode version parameter in HostedMcpToolboxAITool.BuildAddress
- Add XML doc clarifying version pinning is reserved for future use
- Add comment clarifying AddHostedService deduplication safety
- Fix DevTemporaryTokenCredential expiry to use DateTimeOffset.MaxValue
- Fix AgentCard ambiguity in A2AServer sample with using alias
- Add 18 new unit tests for retry handler and ReadMcpToolboxMarkers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* .NET: Hosted agent adapter (#5371)

* Bump preview version to 260420.1 and fix AgentServer package deps

- Bump PackageVersion to 0.0.1-preview.260420.1
- Bump Azure.AI.AgentServer.Core beta.21 -> beta.22 (required by
  Azure.AI.AgentServer.Responses beta.3)
- Replace AgentHostTelemetry.ResponsesSourceName with local constant
  (type made internal in AgentServer.Core beta.22)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix CA1873: guard LogError with IsEnabled check in FoundryToolboxService

Wrap the LogError call with an IsEnabled(LogLevel.Error) guard to satisfy
the CA1873 analyzer rule which flags potentially expensive argument
evaluation when logging is disabled.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* .NET: Hosted agent adapter (#5374)

* Bump preview version to 260420.1 and fix AgentServer package deps

- Bump PackageVersion to 0.0.1-preview.260420.1
- Bump Azure.AI.AgentServer.Core beta.21 -> beta.22 (required by
  Azure.AI.AgentServer.Responses beta.3)
- Replace AgentHostTelemetry.ResponsesSourceName with local constant
  (type made internal in AgentServer.Core beta.22)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix CA1873: guard LogError with IsEnabled check in FoundryToolboxService

Wrap the LogError call with an IsEnabled(LogLevel.Error) guard to satisfy
the CA1873 analyzer rule which flags potentially expensive argument
evaluation when logging is disabled.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Bumping NuGet version

---------

Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* .NET: Hosted agent adapter (#5406)

* Bump preview version to 260420.1 and fix AgentServer package deps

- Bump PackageVersion to 0.0.1-preview.260420.1
- Bump Azure.AI.AgentServer.Core beta.21 -> beta.22 (required by
  Azure.AI.AgentServer.Responses beta.3)
- Replace AgentHostTelemetry.ResponsesSourceName with local constant
  (type made internal in AgentServer.Core beta.22)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix CA1873: guard LogError with IsEnabled check in FoundryToolboxService

Wrap the LogError call with an IsEnabled(LogLevel.Error) guard to satisfy
the CA1873 analyzer rule which flags potentially expensive argument
evaluation when logging is disabled.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Bumping NuGet version

* Restore conditional versioning, remove dev feed, bump Azure.AI.Projects to beta.1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Hosted agent adapter (#5408)

* Bump preview version to 260420.1 and fix AgentServer package deps

- Bump PackageVersion to 0.0.1-preview.260420.1
- Bump Azure.AI.AgentServer.Core beta.21 -> beta.22 (required by
  Azure.AI.AgentServer.Responses beta.3)
- Replace AgentHostTelemetry.ResponsesSourceName with local constant
  (type made internal in AgentServer.Core beta.22)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix CA1873: guard LogError with IsEnabled check in FoundryToolboxService

Wrap the LogError call with an IsEnabled(LogLevel.Error) guard to satisfy
the CA1873 analyzer rule which flags potentially expensive argument
evaluation when logging is disabled.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Bumping NuGet version

* Restore conditional versioning, remove dev feed, bump Azure.AI.Projects to beta.1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR #5312 review comments

- Add comment explaining NU1903 suppression (Microsoft.Bcl.Memory transitive vuln)
- Remove NU1903 from sample/test projects where not needed
- Fix Dockerfile ENTRYPOINT mismatch in Hosted-Workflow-Simple
- Align agent name to 'hosted-workflow-simple' in agent.yaml and README
- Fix Hosted-McpTools README: replace GitHub PAT refs with Microsoft Learn
- Fix session persistence: only persist when client provides conversation ID
- Upgrade IsNullOrEmpty to IsNullOrWhiteSpace for session ID checks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Split Foundry into stable V1 and preview Hosting package

Extract hosted agent functionality from Microsoft.Agents.AI.Foundry into a
new Microsoft.Agents.AI.Foundry.Hosting preview package. This resolves NU5104
build errors caused by the stable Foundry package depending on prerelease
Azure SDK packages (Azure.AI.AgentServer.Responses, Azure.AI.Projects beta).

Changes:
- Create Microsoft.Agents.AI.Foundry.Hosting with VersionSuffix=preview,
  targeting .NET Core only (net8.0/9.0/10.0)
- Move all Hosting/ source files to the new project
- Move ToolboxRecord/ToolboxVersion overloads to FoundryAIToolExtensions
- Revert Azure.AI.Projects to 2.0.0 in Directory.Packages.props;
  Hosting uses VersionOverride for 2.1.0-beta.1
- Clean V1 Foundry csproj: remove beta deps, ASP.NET Core ref, hosting conditionals
- Update 8 hosted agent sample projects to reference Foundry.Hosting
- Split unit tests: ToolboxRecord/ToolboxVersion tests moved to Hosting/
- Add Foundry.Hosting to solution file

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review comments: experimental attrs, doc fixes, token propagation

- Add [Experimental(OPENAI001)] to all 7 public Hosting types per reviewer request
- Fix McpConsentContext XML doc: 'Thread-static' -> 'Async-local' (AsyncLocal
  flows with ExecutionContext, not thread-static)
- Expand UserAgentMiddleware test regex to match prerelease versions (e.g. 1.0.0-rc.4)
- Propagate CancellationToken in AgentFrameworkResponseHandler session save

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove unnecessary MEAI001 suppression from stable Foundry package

MEAI001 was a leftover from when Hosting code lived in the same project.
The stable V1 Foundry package builds clean without it, and suppressing
experimental diagnostics in a released package can hide unintentional
exposure of experimental APIs to consumers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add Foundry.Hosting to release solution filter

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Ben Thomas <ben.thomas@microsoft.com>
2026-04-21 20:25:10 +00:00
57fa8ea902 Python: Fix OpenAIEmbeddingClient to use AsyncOpenAI for /openai/v1 endpoints (#5137)
* Fix OpenAIEmbeddingClient with /openai/v1 endpoint (#5068)

When base_url ends with /openai/v1/ and a credential is provided,
load_openai_service_settings was creating an AsyncAzureOpenAI client.
The Azure SDK rewrites deployment-based endpoints (including /embeddings)
by inserting /deployments/{model}/ into the URL, producing 404s on the
OpenAI-compatible /openai/v1 endpoint.

Use AsyncOpenAI instead of AsyncAzureOpenAI when the resolved base_url
targets /openai/v1, converting the Azure token provider to an async
api_key callable. The responses_mode path is unaffected because the
Responses API (/responses) is not in the SDK's rewrite list.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Fix OpenAIEmbeddingClient to use AsyncOpenAI for /openai/v1 endpoints

Fixes #5068

* Address review feedback: improve test coverage and remove unrelated changes

- Revert unrelated formatting change in test_a2a_agent.py
- Fix test_init_with_openai_v1_base_url_and_api_key_uses_openai_client to
  exercise the Azure settings path (via AZURE_OPENAI_BASE_URL env var)
  instead of the plain OpenAI path, covering the elif api_key branch
- Add _ensure_async_token_provider unit tests for both sync and async
  token providers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #5068: Python: [Bug]: `OpenAIEmbeddingClient` does not work with `/openai/v1` endpoint

---------

Co-authored-by: MAF Dashboard Bot <maf-dashboard-bot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-04-21 19:44:25 +00:00
aa582d021d Python: feat(evals): add ground_truth support for similarity evaluator (#5234)
* feat(evals): add ground_truth support for similarity evaluator

- Include expected_output as ground_truth in Foundry JSONL dataset rows
- Add ground_truth to item schema and data mapping for similarity evaluator
- Add expected_output parameter to evaluate_workflow
- Add similarity Pattern 3 to evaluate_agent and evaluate_workflow samples
- Add tests for ground_truth in dataset, schema, and evaluate_workflow

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: wrap long line to satisfy ruff E501

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-21 19:40:53 +00:00
Jacob AlberandGitHub 8f17067383 .NET: Update .NET package version 1.2.0 (#5364)
* release: Update version for .NET (1.2.0)

* release: Update preview date tag
2026-04-21 19:07:44 +00:00
Jacob AlberandGitHub 267351b760 .NET: Expand Workflow Unit Test Coverage (#5390)
* refactor: remove dead code

* refactor: remove ignore YieldsMessageAttribute

- the correct one to use is YieldsOutputAttribute
- fixes a comment that mistakenly refers to `.YieldsMessage()` which does not exist.

* fix: ChatForwardingExecutor does not use correct role for string messages

- make ChatForwardingExecutor use its configured role for string messages rather than always use ChatRole.User
- add ChatForwardingExecutor tests

* fixup: remove unused attribute

* test: Add tests for failure when .AsAgent used on a non-ChatProtocol workflow

* test: Add FunctionExecutor tests

- also fixes Send and YieldOutput type registration for synchronous output-returning delegates

* test: Suppress CodeCoverage for obsolete names

* fix: Re-add Obsolete attributes

- avoid hard-breaking change
- properly notify users that these attributes get ignored
2026-04-21 18:27:50 +00:00
Peter IbekweandGitHub adcd2d33f5 .NET: Declarative workflows - Gracefully handle agent scenarios when no response is returned (#5376)
* Gracefully handle agent scenarios when no response is returned

* Make relevant object disposable and improve exception handling.
2026-04-21 15:04:49 +00:00
Jacob AlberandGitHub d5777bc546 fix: Duplicate CallIds cause Handoff Message Filtering to fail (#5359)
Some providers, e.g. Gemini, do not use the CallId mechanism to disambiguate simultaneous function calls. This can result in message lists containing multiple turn to fail to filter properly.

The fix is to take advantage of the expectation that Handoff Orchestration is a "single-speaker" flow, which only has a single active AIAgent per "turn" and an agent's turn is not finished until all outstanding function calls are finished.

This allows us to expect that any ambiguous-CallId FunctionCallContent are either in separate turns or will have had a response before the next issued call with the same Id.
2026-04-21 08:14:35 +00:00
b6b191ad9c Python: Add second approval-required tool (set_stop_loss) to concurrent_builder_tool_approval sample (#4875)
* Add set_stop_loss tool to concurrent_builder_tool_approval sample

Add a second approval-gated tool (set_stop_loss) to the concurrent workflow
tool approval sample to demonstrate handling approval requests for different
tools in the same concurrent workflow.

Changes:
- Add set_stop_loss(symbol, stop_price) with approval_mode='always_require'
- Include new tool in both agents' tool lists
- Update agent instructions and prompt to encourage stop-loss usage
- Update docstring to reflect two approval-gated tools
- Update sample output to show mixed approval requests

Fixes #4874

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Print tool name and arguments in concurrent sample's process_event_stream (#4874)

Align process_event_stream in concurrent_builder_tool_approval.py to print
the tool name and arguments when collecting approval requests, matching the
sample output comment and the sequential_builder_tool_approval.py pattern.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add None-guard for function_call access in tool approval sample (#4874)

Add explicit None-checks before accessing function_call.name and
function_call.arguments in concurrent_builder_tool_approval.py. The
function_call field is typed Content | None, so direct attribute access
without a guard could raise AttributeError and required type: ignore
comments. The None-guard is consistent with the pattern used in
_agent_run.py and removes the suppression comments.

Also add a regression test verifying that function_call defaults to None
and that the None-guard pattern is safe.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Apply same function_call None-guard to sibling tool-approval samples (#4874)

Apply the same fix to sequential_builder_tool_approval.py and
group_chat_builder_tool_approval.py, which had the identical pattern
of accessing function_call.name/arguments without a None-guard.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 07:08:50 +00:00
Evan MattsonandGitHub 2c8036779c Python: Bump versions for a release. Update CHANGELOG (#5385)
* Bump versions for a release. Update CHANGELOG

* Bump devui
2026-04-21 15:14:42 +09:00
ce8b6305d8 Python: Foundry hosted agent V2 (#5379)
* Python: Wrapper + Samples 1st (#5177)

* Experiment

* Update dependency and add non streaming

* Add more samples

* Rename samples

* Add invocations

* Comments 1

* Comments 2

* Comments 3

* Improve README

* Add local shell sample

* WIP: Add eval and memory samples

* Update user agent prefix

* Update user agent prefix doc

* Update dependency (#5215)

* Add tests and more content types (#5235)

* Add tests

* fix tests and sample

* Fix formatting

* Remove function approval contents

* Python: Refine samples and upgrade packages (#5261)

* Refine samples and upgrade pacakges

* Upgrade to a new package that fixes a bug

* Update model env var

* Move samples (#5281)

* Python: Upgrade agentserver packages (#5284)

* Upgrade agentserver packages

* Fix new types

* Python: Add special handling for workflows (#5298)

* Add special handling for workflows

* Address comments

* Improve samples (#5372)

* Python: Add more types (#5378)

* Add more type supports

* Upgrade packages

* Remove TODOs in README

* Fix README

* Comments and mypy

* User agent scoped

* Fix README

* Fix pre commit

* Fix pre commit 2

* Fix pre commit 3

* Fix pre commit 4

* Fix pre commit 5

* Fix pre commit 6

* Add azure-monitor-opentelemetry to dev deps

Fixes Samples & Markdown CI failure. The PR's new transitive dep on
azure-monitor-opentelemetry-exporter (via azure-ai-agentserver-core) makes
pyright resolve the azure.monitor.opentelemetry namespace, flipping the
check_md_code_blocks diagnostic for `configure_azure_monitor` from
reportMissingImports (filtered) to reportAttributeAccessIssue (not filtered).
Installing the umbrella azure-monitor-opentelemetry package in dev makes
pyright resolve the symbol correctly, matching the install guidance the
observability README already gives users.

---------

Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
2026-04-21 05:21:27 +00:00
07f4c8a8d6 Python: Expose forwardedProps to agents and tools via session metadata (#5264)
* Expose forwarded_props to agents and tools via session metadata (#5239)

Include forwarded_props from AG-UI request input_data in session.metadata
(agent runner) and function_invocation_kwargs (workflow runner) so that
agents, tools, and workflow executors can access request-level metadata
such as invocation source flags from CopilotKit.

- Add forwarded_props to base_metadata in _agent_run.py when present
- Add 'forwarded_props' to AG_UI_INTERNAL_METADATA_KEYS to filter it
  from LLM-bound client metadata
- Extract forwarded_props in _workflow_run.py and pass via
  function_invocation_kwargs to workflow.run()
- Accept both snake_case and camelCase keys (forwarded_props/forwardedProps)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(ag-ui): pass stream=True as literal to satisfy pyright overload resolution (#5239)

The previous fix passed stream=True via **kwargs dict, which prevented
pyright from resolving the Workflow.run() overload to the streaming
variant. Pass stream=True as an explicit keyword argument so pyright
can correctly infer the ResponseStream return type.

Also remove unused pytest import in test file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address PR review feedback for forwarded_props (#5239)

- Use key-presence checks instead of truthiness for forwarded_props so
  empty dict {} is forwarded correctly
- Gate function_invocation_kwargs on workflow.run() signature inspection
  to avoid TypeError for workflows without **kwargs
- Change _build_safe_metadata to drop (with warning) keys whose
  serialized values exceed 512 chars instead of truncating into invalid
  JSON
- Rewrite metadata tests to exercise _build_safe_metadata directly with
  JSON-decodability and truncation assertions
- Add workflow tests for empty dict forwarded_props, stream=True
  assertion, and signature-gated kwarg dropping

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: add stream=True assertions to CapturingWorkflow tests (#5239)

Guard against accidental removal of the explicit stream=True kwarg
in all forwarded_props CapturingWorkflow test cases.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #5239: Python: Expose forwardedProps to agents and tools via session metadata

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 04:25:45 +00:00
04aaf0c1fe Python: Add support for Foundry Toolboxes (#5346)
* Add support for the Foundry Toolbox in MAF

Introduces a Foundry Toolbox integration: FoundryChatClient gains a
get_toolbox() helper plus select_toolbox_tools(), normalize_tools in
the core package flattens tool-collection wrappers (ToolboxVersionObject
and generic iterables, while leaving Pydantic BaseModel instances
alone), and the new agent_framework.foundry namespace re-exports the
toolbox helpers. Ships with unit tests, a sample, and a design doc.

azure-ai-projects is pinned to the public >=2.0.0,<3.0 range and the
lockfile resolves from public PyPI. The toolbox test module skips when
Toolbox* types are unavailable so CI stays green until the public 2.1.0
SDK lands. OMC tooling directories (.omc/, .omx/) are gitignored.

* Update to latest azure ai projects package

* Improve sample

* Rename ADR to 0025

* Update ADR

* Apply suggestion from @alliscode

Co-authored-by: Ben Thomas <ben.thomas@microsoft.com>

* Improve samples

* Update test

---------

Co-authored-by: Ben Thomas <ben.thomas@microsoft.com>
2026-04-20 23:56:01 +00:00
3e54a689fc Python: Add search tool content for OpenAI responses (#5302)
* Add OpenAI search tool content parsing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix typing

* simplified oai image test

* same for azure

* skip az responses api test

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 13:35:30 +00:00
60af59ba8b .NET: Features/3768-devui-aspire-integration (#3771)
* adds devui integration and samples

* adds unit tests for devui integration

* fix: correct formatting of copyright notice in unit test files

* fixes formatting issues

* fixes build for net8 target

* fixes formatting errors on test apphost

* adds copyright notice to multiple files and removes unnecessary using directives

* Update dotnet/aspire-integration/Aspire.Hosting.AgentFramework.DevUI/DevUIAggregatorHostedService.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/aspire-integration/Aspire.Hosting.AgentFramework.DevUI/DevUIAggregatorHostedService.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/Aspire.Hosting.AgentFramework.DevUI.UnitTests.csproj

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/samples/DevUIIntegration/DevUIIntegration.AppHost/DevUIIntegration.AppHost.csproj

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/aspire-integration/Aspire.Hosting.AgentFramework.DevUI/DevUIAggregatorHostedService.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor project files to use TargetFrameworks instead of TargetFramework for multi-targeting support; add optional port property to DevUIResource class.

* Add unit tests for DevUIAggregatorHostedService; refactor project files for TargetFrameworks support

* Refactor project files to use TargetFrameworks for multi-targeting support in DevUIIntegration samples

* Remove unnecessary using directive for Aspire.Hosting in DevUIAggregatorHostedServiceTests

* merge

* fixes Conversation routing for non-first backends

* add documentation for devui integration sample

* update project references in solution file for improved integration

* fixes package versions post merge

* move Aspire.Hosting.AgentFramework.DevUI to dotnet/src

Move the project from aspire-integration/ to src/ to be consistent
with the location of all other projects in the repo.

* move DevUI sample to samples/05-end-to-end/DevUIAspireIntegration

Move the sample from samples/DevUIIntegration/ to
samples/05-end-to-end/DevUIAspireIntegration/ to match the location
of other end-to-end samples.

* remove unnecessary net472 framework condition from sample csproj files

These projects only target net10.0, so the
Condition="'$(TargetFramework)' != 'net472'" on ItemGroup is unnecessary.

* update sample model name from gpt-4.1 to gpt-5.4

Use a more up-to-date model name in the DevUI integration samples.

* Revert "remove unnecessary net472 framework condition from sample csproj files"

This reverts commit 08cf41253b.

* fix: use TargetFrameworks to override multi-targeting from Directory.Build.props

The parent Directory.Build.props sets TargetFrameworks to net10.0;net472,
which overrides the singular TargetFramework in each csproj. Use the plural
TargetFrameworks property set to net10.0 only to properly override it, and
remove the now-unnecessary net472 condition on ItemGroup.

* fixes aspire config

* fix: update Microsoft.Extensions packages to version 10.0.1

* Address Copilot review feedback on DevUI Aspire integration

- Fix request body dropping in ProxyConversationsAsync: always read the
  body when ContentLength > 0 before routing, then pass it through to
  all proxy calls (previously null was passed when backend was resolved
  from query param or conversation map)
- Fix resource leak: dispose aggregator on startup failure in catch block
- Fix XML docs: accurately describe embedded resource serving behavior
- Remove reflection from DevUIResourceTests (InternalsVisibleTo already set)
- Make sensitive telemetry conditional on Development environment in samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: update chat client version to gpt41 in both EditorAgent and WriterAgent

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 11:12:54 +00:00
Eduard van ValkenburgandGitHub 69894eded8 Python: Flatten hyperlight execute_code output (#5333)
* small fix for hyperlight

* improved sandbox dependency
2026-04-20 08:29:40 +00:00
495e1dad6b Python: Fix CopilotStudioAgent to reuse conversation ID from existing session (#5299)
* Fix CopilotStudioAgent to reuse existing conversation on session (#5285)

CopilotStudioAgent unconditionally called _start_new_conversation() in both
_run_impl and _run_stream_impl, ignoring any existing service_session_id on
the session. Add a guard to only start a new conversation when there is no
existing service_session_id, matching the pattern used by other agents.

Also fix pre-existing pyright reportMissingImports errors for orjson in
file_history_provider samples.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert out-of-scope sample file changes

Remove unrelated orjson type-ignore comment changes from sample files
that were outside the scope of the conversation-ID reuse fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 03:30:54 +00:00
Jacob AlberandGitHub 5777ed26e6 .NET: fix: Add session support for Handoff-hosted Agents (#5280)
* fix: Add session support for Handoff-hosted Agents

In order to better support using `Workflows` hosted as `AIAgents` inside of Handoff workflows, we need to make proper use of AgentSession. This causes potential issues around checkpointing and making sure that we properly compute only the new incoming messages for each agent invocation.

* fix: AgentSession checkpointing using AIAgent's Serialize/Deserialize methods

We cannot rely on implicit serialization through `HandoffHostState` because we are missing type information.

* fix: Thread safety issue in `MultiPartyConversation.AllMessages`

* fix: Enable unwrapping of FunctionResultContent when ExternalRequest was wrapped into FunctionCallContent
2026-04-17 20:15:27 +00:00
52303a8d07 .NET: Add Code Interpreter container file download samples (#5014)
* Add Code Interpreter container file download samples (#3081)

- Add Agent_OpenAI_Step06_CodeInterpreterFileDownload (Public OpenAI)
- Add Agent_Step24_CodeInterpreterFileDownload (Microsoft Foundry)
- Both samples demonstrate downloading cfile_/cntr_ container files
  via ContainerClient instead of the standard Files API
- Update solution file and parent READMEs

* Address review feedback: flatten nested foreach loops using SelectMany

Addresses https://github.com/microsoft/agent-framework/pull/5014#discussion_r3046908449 and https://github.com/microsoft/agent-framework/pull/5014#discussion_r3046920209

---------

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: rogerbarreto <rogerbarreto@users.noreply.github.com>
2026-04-17 16:55:03 +00:00
c85d24da44 .NET: Fix declarative resume edge predicates to recognize both direct and PortableValue-wrapped forms after checkpoint restore (#5323)
* Fix declarative workflows edge predicates after checkpoint restore

* Update test names to  make them clearer and more discoverable.

* Update dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/PortableValuePredicateTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Kit/PortableValuePredicateTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-17 15:18:15 +00:00
b03cb324d5 Python: Add Hyperlight CodeAct package and docs (#5185)
* initial work on code_mode

* updated samples

* updates to codeact

* udpated codeact

* Draft CodeAct ADR and sample updates

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* initial implementation and adr and feature

* Python: Limit Hyperlight wasm backend to Python <3.14

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Fix CI for Hyperlight CodeAct PR

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Run Hyperlight integration when available

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Address Hyperlight review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Simplify Hyperlight file mount inputs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Accept Path host paths in Hyperlight mounts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Fix Hyperlight mount typing for CI

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* temp run integration test

* Python: Strengthen Hyperlight real sandbox tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* added additional tests

* Python: Simplify Hyperlight CodeAct API

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* set tests as non-integration

* Retry Hyperlight allowed-domain registration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Gate Hyperlight integration tests by runtime support

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Hyperlight skip test on Python 3.14

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Delay Hyperlight runtime probe until test execution

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Relax Hyperlight Windows integration stdout assertion

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Scan Hyperlight output directory for artifacts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Retry Hyperlight output artifact collection

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Harden Hyperlight integration output assertions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Retry Hyperlight read-back check in integration test

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Simplify Hyperlight integration write assertion

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Avoid pathlib in Hyperlight integration sandbox

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Use socket network check in Hyperlight sandbox

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Replace blocked Azure AI Search blog link

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clarify Hyperlight guest stdlib limits

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Use _socket in Hyperlight integration sandbox

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Handle Hyperlight mounted file paths

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Broaden Hyperlight sandbox path fallbacks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Search Hyperlight guest mounts recursively

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Split Hyperlight mount coverage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Split Hyperlight live network tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Hyperlight file-write test on Windows

Enable the sandbox filesystem by providing a workspace_root so
/output is mounted. Remove os.path.exists assertion (unsupported
in WASM guest) and fix Content data assertion to use .uri.
Skip the network integration test on Windows where the WASM
sandbox lacks the encodings.idna codec.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review: ADR intro, manual wiring sample, doc clarifications

- Add CodeAct introduction section to ADR for unfamiliar readers
- Clarify 'less runtime efficient' con with specific overhead description
- Add note in Python impl doc clarifying ADR vs impl doc split
- Explain why before_run hooks must be per-run (CRUD, concurrency, approval)
- Rename code_interpreter variable to codeact in E2E sample
- Add manual static wiring sample (codeact_manual_wiring.py)
- Add 'when to use which pattern' guidance to samples README

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR #5185 review comments and add .NET CodeAct design doc

- Fix async callback: _make_sandbox_callback returns sync wrapper with
  thread + asyncio.run() bridge (was broken with real Wasm FFI)
- Fix stale output: clear output_dir before each sandbox.run() call
- Fix blocking event loop: _run_code now async with asyncio.to_thread()
- Revert _agents.py options['tools'] injection (unnecessary; provider
  uses context.extend_tools())
- Revert SessionContext.options docstring back to read-only
- Add real-sandbox test fixtures (shared/restored/fresh)
- Add 8 new real-sandbox tests for callback round-trip, stale output,
  event loop non-blocking, basic execution, stdout/stderr, errors,
  snapshot/restore, and tool registration
- Add comprehensive .NET HyperlightCodeActProvider design document

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update hyperlight README with code snippets and remove Public API section

Replace bare export list with Quick Start code examples covering the
context provider, standalone tool, manual static wiring, and file
mounts / network access patterns.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 00:49:44 +00:00
Jacob AlberandGitHub dbf935b4e3 .NET: fix: Foundry Agents without description in Handoff (#5311)
* fix: Foundry Agents without description in Handoff

Foundry Agents without a description set will return an empty string (rather than null) for the description. This was breaking the fallback logic for `handoffReason`.

* test: Add unit tests
2026-04-16 21:23:01 +00:00
CopilotGitHublokitothcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Jacob Alber
ca580a8316 .NET: Add error checking to workflow samples (#5175)
* Initial plan

* Add WorkflowErrorEvent and ExecutorFailedEvent error checking to all workflow samples

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/c5d77400-d7ed-4fbe-9103-f5d74aabcf2b

Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com>

* Fix if/else if consistency for error event handlers per code review feedback

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/c5d77400-d7ed-4fbe-9103-f5d74aabcf2b

Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com>

* Address PR comments

* fixup: PR comments

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com>
Co-authored-by: Jacob Alber <jaalber@microsoft.com>
2026-04-16 20:03:16 +00:00
Jacob AlberandGitHub 101e07b061 .NET: Add Handoff sample (#5245)
* feat: Add Handoff sample

* docs: Add Handoff sample to readme
2026-04-16 20:02:31 +00:00
aee1acbf8b .NET: Foundry Evals integration for .NET (#4914)
* Foundry Evals integration for .NET

- Core evaluation framework: EvalItem, LocalEvaluator, FunctionEvaluator, EvalChecks
- IAgentEvaluator interface with MeaiEvaluatorAdapter bridge
- AgentEvaluationExtensions for agent.EvaluateAsync() overloads
- FoundryEvals wrapping MEAI quality/safety evaluators
- ConversationSplitters (LastTurn, Full) and IConversationSplitter
- EvalItem.PerTurnItems() for multi-turn decomposition
- HasImageContent for multimodal content detection
- WorkflowEvaluationExtensions for per-agent workflow evaluation
- 7 eval samples mirroring Python parity:
  02-agents/Evaluation: SimpleEval, ExpectedOutputs, Multimodal
  03-workflows/Evaluation: WorkflowEval
  05-end-to-end/Evaluation: FoundryQuality, MixedProviders, ConversationSplits
- Comprehensive unit tests (1958 passing)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Rewrite FoundryEvals to use real Foundry Evals API

Replace MEAI evaluator shim with actual OpenAI EvaluationClient protocol
methods. FoundryEvals now creates eval definitions, submits runs, polls
for completion, and fetches per-item results server-side.

- New constructor: FoundryEvals(AIProjectClient, model, evaluators)
- Add FoundryEvalConverter for MEAI ChatMessage -> Foundry JSON format
- Add EvalId, RunId, ReportUrl to AgentEvaluationResults
- All 20 built-in evaluator constants now work (agent, tool, quality, safety)
- Remove Microsoft.Extensions.AI.Evaluation.Quality/Safety dependencies
- Update all samples for new constructor (no more ChatConfiguration)
- Replace BuildEvaluators tests with ResolveEvaluator tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add response output to CustomEvals and ExpectedOutputs samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review: pagination, validation, error handling, tests

FoundryEvals fixes:
- Add pagination for output items (has_more/after cursor)
- Add guard clauses for pollIntervalSeconds/timeoutSeconds <= 0
- Fix double TryGetProperty for passed field parsing
- Throw on all-tool-evaluators with no tool definitions
- Fix XML doc (default 300s, not 180s)

New tests (30 added, 1989 total):
- EvalChecks: NonEmpty, ContainsExpected (pass/fail/skip/case),
  HasImageContent, ToolCallsPresent
- FoundryEvalConverter: ConvertMessage (text, image, function call,
  function results fan-out, empty fallback, mixed content),
  ConvertEvalItem, BuildTestingCriteria (quality/agent/tool/groundedness
  data mappings), BuildItemSchema

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix review: null-refs, Data.ToString() bug, ContainsExpected, add tests

- Fix NullReferenceException in sample Response display (pattern matching)
- Fix WorkflowEvaluationExtensions Data?.ToString() producing type names
  instead of message text (pattern-match ChatMessage/AgentResponse/list)
- Change EvalChecks.ContainsExpected to return Passed=false when no
  ExpectedOutput (was silently passing, masking misconfiguration)
- Add EvalItem constructor tests with LastTurn/Full/null splitters
- Add FoundryEvalConverter.ConvertMessage DataContent (base64 image) test
- Add ExtractAgentData tests with ChatMessage, list, and AgentResponse data

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix review: conversation fidelity, eval caching, fallback tests

- WorkflowEvaluationExtensions: preserve full response messages (tool calls,
  intermediate) instead of synthetic 2-message conversation. Cast completed
  Data to AgentResponse and use Messages when available, fallback to text.
- FoundryEvals: cache evalId per schema shape (hasContext, hasTools) so
  subsequent EvaluateAsync calls create runs under the same eval definition.
- MeaiEvaluatorAdapter: code already correctly passes queryMessages (not full
  conversation) to IEvaluator — no change needed, verified by inspection.
- Add tests: AgentResponse full messages preservation, unknown object
  ToString() fallback for ExtractAgentData.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Rename AzureAI→Foundry: move eval files, update references

- Move FoundryEvals.cs and FoundryEvalConverter.cs from
  Microsoft.Agents.AI.AzureAI to Microsoft.Agents.AI.Foundry
- Update namespace from AzureAI to Foundry in both files
- Add explicit usings required by Foundry project (no implicit usings)
- Move FoundryEvalConverter tests to Foundry.UnitTests project
  (avoids ReplacingRedactor type conflict from dual project refs)
- Update all sample csproj references and using statements
- Remove Foundry project reference from AI UnitTests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* PR review round 4: wire up tool extraction, remove eval cache, fix null safety

- BuildEvalItem: extract tools from agent via GetService<ChatOptions>() into EvalItem.Tools (Python parity)
- FoundryEvals: remove eval ID cache - each call creates fresh definition (matches Python behavior)
- FoundryEvals: replace null-forgiving operators with descriptive InvalidOperationException
- MixedProviders sample: remove unnecessary explicit PackageReferences (transitively provided)
- FoundryEvalConverter: document that tool results take precedence over text content
- Add LocalEvaluator zero-checks test documenting 0 metrics = failed behavior

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python-dotnet parity: 9 feature gaps filled

New checks:
- ToolCallArgsMatch() — verify tool call names + argument subset match
- ToolCalledCheck(ToolCalledMode.Any, ...) — match any of the specified tools
- ToolCalledMode enum (All/Any)

FoundryEvals enhancements:
- Default evaluators now [Relevance, Coherence, TaskAdherence] (was Relevance, Coherence)
- Auto-add ToolCallAccuracy when items have tool definitions
- EvaluateTracesAsync — evaluate by response_ids, trace_ids, or agent_id
- EvaluateFoundryTargetAsync — evaluate deployed Foundry targets

Result type enrichment:
- AgentEvaluationResults: added Status, Error, PerEvaluator, DetailedItems
- New EvalItemResult/EvalScoreResult/PerEvaluatorResult types
- FoundryEvals populates all new fields from API responses

Workflow fix:
- Skip internal executors (_*, input-conversation, end-conversation, end)

Tests: 8 new tests covering ToolCallArgsMatch, ToolCalledMode.Any, internal executor filtering

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add MeaiEvaluatorAdapter and PerTurnItems edge case tests

- 3 tests for MeaiEvaluatorAdapter: query message forwarding, synthetic
  response fallback, multiple items aggregation
- 3 tests for EvalItem.PerTurnItems: empty conversation, no user messages,
  system+assistant only
- StubEvaluator and StubChatClient test helpers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Blocking link check for outdated package in DevUI.

* Replace Dictionary<string, object> payloads with typed wire models

Introduce internal FoundryEvalWireModels.cs with compile-time-safe types
for the OpenAI Evals API wire format. The OpenAI .NET SDK (2.9.1) only
provides protocol-level methods with BinaryContent/ClientResult — no
typed request models. These internal models replace scattered dictionary
literals with [JsonPropertyName]-annotated classes, giving:

- Compile-time safety (typos become build errors)
- Single point of change when the API evolves
- IntelliSense discoverability
- Cleaner serialization via JsonPolymorphic for content items

Models: WireContentItem hierarchy (text, image, tool_call, tool_result),
WireMessage, WireEvalItemPayload, WireTestingCriterion, WireItemSchema,
WireCreateEvalRequest, WireCreateRunRequest, and data source variants.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Skip metric when Foundry returns neither score nor passed

When an evaluator returns no score and no passed value, the previous
code created BooleanMetric(name, false), which falsely failed items
via ItemPassed. Now we skip the MEAI metric entirely for indeterminate
results — the raw data remains available in DetailedItems for diagnostics.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR #4914 review comments: fix tool evaluator bug and add tests

- Fix duplicate ToolCallAccuracy: resolve evaluator names before checking
  against ToolEvaluators set (Comment 2)
- Make FilterToolEvaluators internal for testability; add tests for the
  ArgumentException edge case when all evaluators are tool-type (Comment 3)
- Add CancellationToken test for LocalEvaluator (Comment 4)
- Add EvaluateAsync integration test on Run with sequential workflow and
  per-agent SubResults verification (Comment 5)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Peter's review comments on PR #4914

- Add trailing newline to Evaluation_FoundryQuality.csproj (Comment 6)
- Make evaluator name lookups case-insensitive: switch BuiltinEvaluators,
  ToolEvaluators, AgentEvaluators, and ResolveEvaluator's StartsWith check
  from Ordinal to OrdinalIgnoreCase (Comment 7)
- Add Trace.TraceWarning when Foundry returns fewer results than submitted
  items, indicating expected vs actual count before padding (Comment 8)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add Microsoft.Extensions.AI.Evaluation packages to Directory.Packages.props

These were removed in #5269 as unused, but are needed by the Foundry
and core evaluation integration added in this PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-16 19:40:07 +00:00
L. Elaine DazzioandGitHub 91e34358eb Python: Feat: Add finish_reason support to AgentResponse and AgentResponseUpdate (#5211)
* feat: add finish_reason support to AgentResponse and AgentResponseUpdate

Add finish_reason field to AgentResponse and AgentResponseUpdate classes,
propagate it through _process_update() and map_chat_to_agent_update(),
and add comprehensive unit tests.

Fixes #4622

* feat: add finish_reason to AgentResponse and AgentResponseUpdate

* style: add copyright header to test_finish_reason.py

* docs: add finish_reason to AgentResponse and AgentResponseUpdate docstrings

* refactor: move finish_reason tests into test_types.py per review feedback

Move all finish_reason test cases from the separate test_finish_reason.py
file into test_types.py as requested by eavanvalkenburg. Tests are placed
in a new '# region finish_reason' section at the end of the file.

* fix: use model instead of model_id in _process_update

Address PR review feedback from @eavanvalkenburg — ChatResponse and
ChatResponseUpdate both use 'model', not 'model_id'.

* fix: resolve SIM102 lint error in _process_update

Combine nested if statements for AgentResponse finish_reason check
to satisfy ruff SIM102 rule, with line wrapping to stay under 120 chars.

* fix: resolve pyright reportArgumentType in map_chat_to_agent_update

Add type: ignore[arg-type] for FinishReason NewType widening when
passing ChatResponseUpdate.finish_reason to AgentResponseUpdate.
Matches existing patterns in the codebase (40+ similar ignores).
2026-04-16 19:39:09 +00:00
90a633967c Python: Fix Gemini client support for Gemini API and Vertex AI (#5258)
* Add Gemini and Vertex AI client support

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Gemini PR review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* removed sample run readme part

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-04-16 19:38:50 +00:00
Jacob AlberandGitHub c14beedb3a test: Add Handoff composability test (#5208) 2026-04-16 16:36:09 +00:00
Kartik MadanandGitHub 43d98974d3 fix: propagate A2A metadata with namespaced key in additional_properties (#5240) (#5256) 2026-04-16 15:22:39 +00:00
60da0ffb48 .NET: Improve local release build perf by only formatting for one build target framework (#5266)
* Improve local release build perf by only formatting for one build target framework

* Update dotnet/Directory.Build.targets

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-16 15:21:33 +00:00
westeyandGitHub a2044829b1 .NET: Update Microsoft.Extensions.AI to 10.5.0 and OpenAI to 2.10.0 and remove unused refs (#5269)
* Update versions of System, Microsoft.Extensions and OpenAI packages

* Remove unused package references

* Remove further unused references
2026-04-16 11:03:51 +00:00
435c66e9c9 Python: Handle url_citation annotations in FoundryChatClient streaming responses (#5071)
* Fix url_citation annotations dropped in streaming (#5029)

Add url_citation branch to the streaming annotation handler in
_parse_chunk_from_openai, mirroring the existing non-streaming path.
The handler creates an Annotation with type='citation', title, url,
and annotated_regions (TextSpanRegion), wrapped in Content.from_text.

Update test_streaming_annotation_added_with_unknown_type to use a
truly unknown type, and add new tests for url_citation (with and
without url).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #5029: Python: [Bug]: url_citation annotations silently dropped in Foundry streaming (SharePoint grounding citations lost)

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-04-16 09:33:04 +00:00
Roger BarretoandGitHub 52d50be9e0 Bump Anthropic SDK to 12.13.0 and Anthropic.Foundry to 0.5.0 (#5279)
- Update Anthropic from 12.11.0 to 12.13.0
- Update Anthropic.Foundry from 0.4.2 to 0.5.0
- Change Anthropic project from release candidate to preview
- Add new IBetaService members (Agents, Environments, Sessions, Vaults) to test mock
2026-04-16 09:19:36 +00:00
d20f9b5f97 Add AgentExecutorResponse.with_text() to preserve conversation history through custom executors (#5255)
Fixes #5246

When a custom @executor transforms agent output and sends a plain str,
the downstream AgentExecutor.from_str handler loses the full conversation
context. This adds a with_text() helper that creates a new
AgentExecutorResponse with replaced text while preserving the prior
conversation chain, so AgentExecutor.from_response is invoked instead.

- Add with_text(text) method to AgentExecutorResponse dataclass
- Add 3 regression tests in test_full_conversation.py

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-04-16 08:39:19 +00:00
Peter IbekweandGitHub 87a8fa2a9d .NET: Fix intermittent checkpoint-restore race in in-process workflow runs (#5134)
* Improve workflow unit tests

* Update test name prefix for clarity.

* Update tests to surface any errors.

* fix check-point restore-time race in off-thread workflow event stream

* Fixes an intermittent checkpoint-restore race in in-process workflow runs.
2026-04-16 04:20:45 +00:00
Tao ChenandGitHub 8f7fd9525d Python: Add OpenAI types to default checkpoint encoding allow list (#5297)
* Add OpenAI types to default checkpoint encoding allow list

* Address comments
2026-04-16 12:58:28 +09:00
69697065ab Python: Add context_providers and description to workflow.as_agent() (#4651)
* Add context_providers and description to `workflow.as_agent()`

* Add default workflow name and description

* Positional

* Move import

---------

Co-authored-by: Tao Chen <taochen@microsoft.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-04-16 02:47:29 +00:00
Evan MattsonandGitHub fe4cd3cddc Revert to public MCP server and skip on transient upstream errors (#5296)
The local MCP server can't be used for hosted tools tests because
Anthropic's backend needs to reach the MCP URL from their infrastructure
(not localhost on the CI runner). Revert to learn.microsoft.com/api/mcp
but catch BadRequestError, InternalServerError, APIConnectionError, and
APITimeoutError and pytest.skip so upstream outages don't block the
merge queue.
2026-04-16 11:46:49 +09:00
Evan MattsonandGitHub 611230cc8e Python: improve misc-integration test robustness (#5295)
* Python: use local MCP server for hosted tools test and broaden image assertion

The hosted tools integration test was hitting rate limits on the external
learn.microsoft.com MCP server, causing persistent failures that retries
couldn't recover from. Switch to the local MCP server already spun up in
CI via LOCAL_MCP_URL, skipping when the env var isn't set.

Also broaden the image description assertion to accept common synonyms
(cottage, mansion, villa, etc.) instead of just "house", since the model
legitimately uses varied vocabulary for the same image.

* Address review feedback: validate LOCAL_MCP_URL scheme and use word boundaries

- Skip hosted tools test when LOCAL_MCP_URL lacks http/https scheme,
  matching the pattern used in test_mcp.py.
- Use regex word boundaries for image assertion to avoid false matches
  like "villain" matching "villa".
2026-04-16 11:34:28 +09:00
Evan MattsonandGitHub f112150cfb Python: bump misc-integration retry delay to 30s (#5293)
The misc-integration job (Anthropic, Ollama, MCP) frequently fails on merge to main when the upstream MCP server (e.g. learn.microsoft.com/api/mcp) returns a transient rate-limit error. The previous 5s retry delay is too short to ride out the upstream backoff window, so all retries fail and the merge queue is blocked. Bumping to 30s gives the upstream a chance to recover before pytest-retry re-runs the test.
2026-04-16 10:03:00 +09:00
ff05c22c58 Python: add experimental file history provider (#5248)
* add experimental file history provider

* Improve file history provider writes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* typo

* cleanup

* cleanup

* fix in readme

* added security messages

* Refine file history provider locking

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* added additional sample

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 22:23:37 +00:00
eab7f09d03 Forward provider config to SessionConfig in GitHubCopilotAgent (fixes #5190) (#5195)
Co-authored-by: Sergey Borisov <sergey.borisov@dataimpact.io>
2026-04-15 22:08:01 +00:00
68b93641b6 Python: Bump agent-framework-devui to 1.0.0b260414 for release (#5259)
Update devui version and changelog for the streaming memory fix release.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 18:22:15 +00:00
2b251d904f Python: Fix reasoning replay when store=False (#5250)
* fix reasoning content when store=False

* Remove accidental worktree entries

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* remove local session sample

* removed left over files

* Add attribution override regression test

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 17:30:12 +00:00
485af07b8c Python: Add GeminiChatClient (#4847)
* Add agent-framework-gemini package

* Add AGENTS.md documentation

* Add LICENSE file

* Add README.md for agent-framework-gemini package

* Add Google Gemini API keys to .env.example

* Add Google Gemini chat client implementation

* Add tests for GeminiChatClient

* Add Google Gemini agent examples

* Fix client inheritence order

* Update Gemini agent examples

* Update documentation

* Update AGENTS.md

* Add tests for JSON string handling in GeminiChatClient

* Add final response assembly test in GeminiChatClient

* Add tests for handling empty candidates in GeminiChatClient

* Improve Pydantic response handling in GeminiChatClient

* Add tests for function result resolution and callable tool normalization

* Add test for function result resolution when call_id is generated

* Refactor GeminiChatClient to correct inheritance order

Also updates constructor parameter order for environment file handling

* Enhance documentation and clarify Gemini-specific fields

* Update ThinkingConfig with new attributes and type

* Add tests for GoogleSearch and GoogleMaps configs

* Suppress valid-type mypy error on GeminiChatOptionsT

* Move service_url method near overrides

* Order _prepare_config kwargs by base then Gemini-specific

* Use FunctionCallingConfigMode for clarity and type safety

* Fix code_execution doc

* Add agent-framework-gemini to project dependencies

* Remove package from core dependencies

Initial release will be done without agent-framework-gemini in
core[all].

* Move integration tests into one file

* Remove __init__.py file from gemini tests directory

* Introduce RawGeminiChatClient as lightweight chat client

Updated GeminiChatClient to inherit from RawGeminiChatClient, maintaining full functionality with added features.

* Updated variable names from `model_id` to `model`

Across the codebase, including environment variables and client initialization. Adjusted related tests and sample scripts to reflect this change, ensuring consistency in the usage of the Gemini model identifier.

* Update AGENTS.md

* Update Gemini package to alpha status

* Fix docstrings in Gemini tests

* Change 'model_id' to 'model' in response handling

* Fix model property change in response handling

* Add built-in tool factory methods to Gemini client

Replaces boolean tool options (code_execution, google_search_grounding,
google_maps_grounding) with static factory methods that return types.Tool
objects: get_code_interpreter_tool, get_web_search_tool, get_mcp_tool,
get_file_search_tool, and get_maps_grounding_tool.

Simplifies _prepare_tools to a single translation boundary between
FunctionTool (framework) and FunctionDeclaration (Gemini API), with
types.Tool objects passed through unchanged.

* Surface code execution parts

_parse_parts now maps executable_code and code_execution_result
parts to text Content objects so callers can see the code run
and its output. Unknown part types log at debug level rather than
being silently dropped.

* Update Gemini client documentation

* Unify Gemini model name

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Update Agent Framework core version

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Add Python 3.14 in classifiers

* Replace kwargs with parameters in tool factories

* Refactor chat options handling in Gemini client

* Add tests for handling unknown and consumed keys

* Update Gemini documentation

Now reflects new options and built-in tool factory methods

* Change build system to flit

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Fix build system in pyproject.toml

* Fix type checking for generate_content_stream

---------

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-04-14 10:18:26 +00:00
Dineshsuriya DandGitHub 64c68ca857 Python: Skip get_final_response in OTel _finalize_stream when stream errored (#5232)
* Python: Skip get_final_response in OTel _finalize_stream when stream errored

When a streaming error occurs, _finalize_stream (a cleanup hook registered by
AgentTelemetryLayer) was unconditionally calling get_final_response(), which
triggers all registered result hooks including after_run context providers.
This caused providers to fire incorrectly on error paths.

Guard against this by checking result_stream._consumed: True only after
StopAsyncIteration (normal completion), False when an exception was raised.
The fix applies to both the chat client and agent telemetry layers.

Closes #5231

* Python: Expose consumed/stream_error on ResponseStream and capture error in OTel span

Address Copilot review feedback on #5232:

- Add `_stream_error: Exception | None` to ResponseStream, set in __anext__'s
  except branch so cleanup hooks can inspect the failure.
- Expose public `consumed` and `stream_error` properties to avoid coupling
  observability.py to private stream internals.
- Update both _finalize_stream closures (chat and agent layers) to use the
  public properties and call capture_exception() with the stream error before
  returning early, ensuring the OTel span records the failure rather than
  closing silently.

* Python: Address Copilot review feedback on stream error handling

- Use stream_error is not None as the guard in _finalize_stream instead of
  not consumed, so the early-return path is keyed precisely to actual errors
  rather than any non-normal completion state.
- Clear _stream_error after _run_cleanup_hooks() completes to avoid retaining
  the exception traceback (and any large object graphs it references) on the
  stream instance beyond the cleanup phase.

* Python: Remove consumed/stream_error properties, use private attrs directly

Per review feedback: since observability.py and _types.py are in the same
package, accessing _stream_error directly is fine and the public properties
are unnecessary.

* Python: Fix Pyright reportPrivateUsage via inline ignore comments

Keep _stream_error private (consistent with rest of ResponseStream), and
suppress reportPrivateUsage at the call sites in observability.py with
inline pyright: ignore comments — access is intentional within the package.
2026-04-14 09:30:31 +00:00
Eduard van ValkenburgandGitHub 98e17764a4 Python: Fix DevUI streaming memory growth and add cross-platform regression coverage (#5221)
* fix for memory leak in devui

* update async sleep

* remove old func
2026-04-14 09:27:52 +00:00
Tao ChenandGitHub 7bb0feca59 Python: Move InMemory history provider injection to the first invocation (#5236)
* Move InMemory history provider injection to the first invocation

* Add tests
2026-04-14 07:13:42 +00:00
f183f888a3 Python: AG-UI deterministic state updates from tool results (#5201)
* AG-UI deterministic state updates from tool results

* fix(ag-ui): address PR #5201 review comments

1. Add missing AGUIEventConverter, AGUIHttpService, __version__ to
   _IMPORTS in core ag_ui lazy-export list to match the .pyi stub.

2. Coalesce predictive and deterministic state snapshots into a single
   StateSnapshotEvent when both mechanisms are active on the same tool
   result, reducing redundant snapshot traffic.

3. Update state_update() docstring to clarify that a predictive snapshot
   may be emitted before the deterministic one when predict_state_config
   is active.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-14 04:58:09 +00:00
3c31ac28b5 Python: Fix HandoffBuilder dropping function-level middleware when cloning agents (#5220)
* Fix HandoffBuilder dropping function-level middleware when cloning agents (#5173)

_clone_chat_agent() was using agent.agent_middleware (agent-level only)
instead of agent.middleware (all types), which silently dropped any
function middleware registered on the original agent.

Changed to use agent.middleware to preserve all middleware types
(agent, function, and chat) during cloning.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Fix HandoffBuilder dropping function-level middleware when cloning agents

Fixes #5173

* Fix false-positive middleware regression test (#5173)

The test used isinstance(m, FunctionMiddleware) which matched
_AutoHandoffMiddleware (always appended during build) instead of the
user's @function_middleware decorator. Assert directly that
tracking_middleware is present in the cloned agent's middleware list.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #5173: Python: [Bug]: HandoffBuilder drops function-level middleware when cloning agents

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-14 04:52:03 +00:00
1b95e8585d Python: Add allowed_checkpoint_types support to CosmosCheckpointStorage for parity with FileCheckpointStorage (#5202)
* Python: Add allowed_checkpoint_types support to CosmosCheckpointStorage (#5200)

Add allowed_checkpoint_types parameter to CosmosCheckpointStorage for
parity with FileCheckpointStorage. This ensures both providers use the
same restricted pickle deserialization by default.

Changes:
- Accept allowed_checkpoint_types kwarg in __init__, stored as frozenset
- Convert _document_to_checkpoint from @staticmethod to instance method
- Forward allowed_types to decode_checkpoint_value on all load paths
- Update class docstring to describe the new parameter
- Add tests covering built-in safe types, app type opt-in/blocking,
  and all load paths (load, list_checkpoints, get_latest)
- Add changelog entry noting the breaking behavior change

BREAKING CHANGE: CosmosCheckpointStorage now uses restricted pickle
deserialization by default. Checkpoints containing application-defined
types will require passing those types via allowed_checkpoint_types.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Add `allowed_checkpoint_types` support to `CosmosCheckpointStorage` for parity with `FileCheckpointStorage`

Fixes #5200

* Address PR review: add pickle security warning and fix docstring examples

- Reintroduce explicit security warning about pickle deserialization risks
- Convert Example:: block to .. code-block:: python with imports for
  consistency with other docstring examples
- Note: PR title should be updated to include [BREAKING] prefix per
  changelog convention (comment #3, requires GitHub UI change)

Fixes #5200

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-14 02:20:55 +00:00
CopilotGitHubSergeyMenshykhcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
b89adb280b Python: skill name validation improvements (#4530)
* Initial plan

* Port .NET validation improvements to Python skills: reject consecutive hyphens and enforce directory name match

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix E501 lint error: split long error message string in _validate_skill_metadata

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
2026-04-13 23:39:09 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
913397492f Bump pygments from 2.19.2 to 2.20.0 in /python (#4978)
Bumps [pygments](https://github.com/pygments/pygments) from 2.19.2 to 2.20.0.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.19.2...2.20.0)

---
updated-dependencies:
- dependency-name: pygments
  dependency-version: 2.20.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 22:52:34 +00:00
952e685e17 Python: Fix python-feature-lifecycle skill YAML frontmatter (#5226)
* Fix python-feature-lifecycle skill YAML frontmatter

Remove copyright comment that preceded the YAML frontmatter delimiter,
which prevented the skill from loading. The --- block must be the very
first line of SKILL.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: update broken eslint-react plugin links in devui README

The upstream eslint-react repo moved plugins from packages/plugins/
to the top-level plugins/ directory, causing 404 errors detected by
linkspector CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-13 22:28:06 +00:00
b1fb63eb81 .NET: Update AGUI service to support session storage (#5193)
* Update AGUI service to support session storage

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Address PR comments

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-13 18:03:51 +00:00
Jacob AlberandGitHub 76fe7319e0 .NET: feat: Refactor Handoff Orchestration and add HITL support (#5174)
* feat: Refactor Handoff Orchestration and add HITL support

* Change HandoffAgentExecutor to use factory-based instantiation
* Extract shared request collection logic in AIAgentUnservicedRequestsCollector
* Refactor HandoffAgentExecutor to use the "ContinueTurn" pattern as in AIAgentHostExecutor

* fix: Remove '$' from exception strings
2026-04-13 14:59:17 +00:00
westeyandGitHub 39b560f83c Add missing path to verify-samples run checkout (#5194) 2026-04-13 11:00:31 +00:00
3e864cdb4c .NET: Update version to 1.1.0 (#5204)
* Update version to 1.1.0

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-10 15:28:00 +01:00
14d2ab3262 Standardize file skills terminology on 'directory' (#5205)
Rename authored identifiers, XML docs, log messages, and comments
from 'folder' to 'directory' across the file skills codebase for
consistency with the agentskills.io specification and .NET conventions.

Public API changes (experimental):
- ScriptFolders → ScriptDirectories
- ResourceFolders → ResourceDirectories

.NET BCL API calls (Directory.Exists, Path.GetDirectoryName, etc.)
were already using 'directory' and are unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 15:27:45 +01:00
e5f7b9c260 .NET: Support reflection for discovery of resources and scripts in class-based skills (#5183)
* support reflection for discovery of resources and scripts in class-based skills

* fix format issues

* refactor samples to use reflection

* Validate resource member signatures during discovery

Add discovery-time validation in AgentClassSkill.DiscoverResources() to
fail fast when [AgentSkillResource] is applied to members with incompatible
signatures:

- Reject indexer properties (getter has parameters)
- Reject methods with parameters other than IServiceProvider or
  CancellationToken

Throws InvalidOperationException with actionable error messages instead of
allowing silent runtime failures when ReadAsync invokes the AIFunction with
no named arguments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* prevent duplicates

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 11:56:28 +01:00
Evan MattsonandGitHub 4a36f10888 Python: Bump Python version to 1.0.1 for a release (#5196)
* Bump Python version to 1.1.0 for a release

* Fix changelog

* 1.0.1 instead of 1.1.0

* Update CHANGELOG.md

* update version and changelog

* Bump lower bounds
2026-04-10 12:23:21 +09:00
d4036c5aef Python: Migrate GitHub Copilot package to SDK 0.2.x (#5107)
* Python: Migrate GitHub Copilot package to SDK 0.2.x

Replace all imports from the non-existent copilot.types module with
correct SDK 0.2.x module paths (copilot.session, copilot.client,
copilot.tools, copilot.generated.session_events). Fix PermissionRequest
attribute access from dict-style .get() to dataclass attribute access.
Add OTel telemetry support to Copilot samples via configure_otel_providers
and document new telemetry environment variables in samples README.

* Python: Fix remaining copilot.types import in sample validation script

* Python: Include model in default_options for telemetry span attributes

* Python: Address review feedback on log_level and session kwargs typing

* Python: Scope PR to SDK 0.2.x migration only, remove net-new OTel features

- Remove RawGitHubCopilotAgent split and AgentTelemetryLayer inheritance
- Remove TelemetryConfig plumbing and OTLP/file telemetry settings
- Remove configure_otel_providers() calls from samples
- Remove telemetry env var rows from samples README
- Retain only: import path fixes, PermissionRequest attribute access fix,
  log_level default fix, session kwargs typed fix, dependency pin

* Python: Update tests for SDK 0.2.x API changes

- SubprocessConfig replaces CopilotClientOptions dict
- create_session and resume_session now use keyword args
- send and send_and_wait take plain string prompt instead of MessageOptions
- on_permission_request is always required; deny-all fallback replaces omission

* Python: Pin github-copilot-sdk to >=0.2.0,<=0.2.0

Tighten the upper bound from <0.3.0 to <=0.2.0 to avoid pulling in 0.2.1+
which has breaking API changes relative to 0.2.0. The lower bound stays at
>=0.2.0 since this migration requires the 0.2.x import paths; 0.1.x would
fail at import time.

* Python: Pin github-copilot-sdk to >=0.2.1,<=0.2.1

---------

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-04-10 01:07:14 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Evan Mattson
790a759dbf Bump mcp from 1.26.0 to 1.27.0 in /python (#5117)
Bumps [mcp](https://github.com/modelcontextprotocol/python-sdk) from 1.26.0 to 1.27.0.
- [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases)
- [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md)
- [Commits](https://github.com/modelcontextprotocol/python-sdk/compare/v1.26.0...v1.27.0)

---
updated-dependencies:
- dependency-name: mcp
  dependency-version: 1.27.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-04-10 00:13:45 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a172313ec3 Bump mcp[ws] from 1.26.0 to 1.27.0 in /python (#5119)
Bumps [mcp[ws]](https://github.com/modelcontextprotocol/python-sdk) from 1.26.0 to 1.27.0.
- [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases)
- [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md)
- [Commits](https://github.com/modelcontextprotocol/python-sdk/compare/v1.26.0...v1.27.0)

---
updated-dependencies:
- dependency-name: mcp[ws]
  dependency-version: 1.27.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 00:13:40 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Evan Mattson
e8757cebde Bump cryptography from 46.0.6 to 46.0.7 in /python (#5176)
Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.6 to 46.0.7.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/46.0.6...46.0.7)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-04-10 00:13:32 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
eea543e697 Bump vite (#5132)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 7.3.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 00:11:48 +00:00
4dbe696e0e Python: Restrict persisted checkpoint deserialization by default (#4941)
* Harden Python checkpoint persistence defaults

Add RestrictedUnpickler to _checkpoint_encoding.py that limits which
types may be instantiated during pickle deserialization.  By default
FileCheckpointStorage now uses the restricted unpickler, allowing only:

- Built-in Python value types (primitives, datetime, uuid, decimal,
  collections, etc.)
- All agent_framework.* internal types
- Additional types specified via the new allowed_checkpoint_types
  parameter on FileCheckpointStorage

This narrows the default type surface area for persisted checkpoints
while keeping framework-owned scenarios working without extra
configuration.  Developers can extend the allowed set by passing
"module:qualname" strings to allowed_checkpoint_types.

The decode_checkpoint_value function retains backward-compatible
unrestricted behavior when called without the new allowed_types kwarg.

Fixes #4894

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: resolve mypy no-any-return error in checkpoint encoding

Add explicit type annotation for super().find_class() return value
to satisfy mypy's no-any-return check.

Fixes #4894

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Simplify find_class return in _RestrictedUnpickler (#4894)

Remove unnecessary intermediate variable and apply # noqa: S301 # nosec
directly on the super().find_class() call, matching the established
pattern used on the pickle.loads() call in the same file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4894: Python: Harden Python checkpoint persistence defaults

* Restore # noqa: S301 on line 102 of _checkpoint_encoding.py (#4894)

The review feedback correctly identified that removing the # noqa: S301
suppression from the find_class return statement would cause a ruff S301
lint failure, since the project enables bandit ("S") rules. This
restores consistency with lines 82 and 246 in the same file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4894: Python: Harden Python checkpoint persistence defaults

* Address PR review comments on checkpoint encoding (#4894)

- Move module docstring to proper position after __future__ import
- Fix find_class return type annotation to type[Any]
- Add missing # noqa: S301 pragma on find_class return
- Improve error message to reference both allowed_types param and
  FileCheckpointStorage.allowed_checkpoint_types
- Add -> None return annotation to FileCheckpointStorage.__init__
- Replace tempfile.mktemp with TemporaryDirectory in test
- Replace contextlib.suppress with pytest.raises for precise assertion
- Remove unused contextlib import

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR #4941 review comments: fix docstring position and return type

- Move module docstring before 'from __future__' import so it populates
  __doc__ (comment #4)
- Change find_class return annotation from type[Any] to type to avoid
  misleading callers about non-type returns like copyreg._reconstructor
  (comment #2)

Comments #1, #3, #5, #6, #7, #8 were already addressed in the current code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4894: review comment fixes

* fix: use pickle.UnpicklingError in RestrictedUnpickler and improve docstring (#4894)

- Change _RestrictedUnpickler.find_class to raise pickle.UnpicklingError
  instead of WorkflowCheckpointException, since it is pickle-level concern
  that gets wrapped by the caller in _base64_to_unpickle.
- Remove now-unnecessary WorkflowCheckpointException re-raise in
  _base64_to_unpickle (pickle.UnpicklingError is caught by the generic
  except Exception handler and wrapped).
- Expand decode_checkpoint_value docstring to show a concrete example of
  the module:qualname format with a user-defined class.
- Add regression test verifying find_class raises pickle.UnpicklingError.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address PR #4941 review comments for checkpoint encoding

- Comment 1 (line 103): Already resolved in prior commit — _RestrictedUnpickler
  now raises pickle.UnpicklingError instead of WorkflowCheckpointException.

- Comment 2 (line 140): Add concrete usage examples to decode_checkpoint_value
  docstring showing both direct allowed_types usage and FileCheckpointStorage
  allowed_checkpoint_types usage. Rename 'SafeState' to 'MyState' across all
  docstrings for consistency, making it clear this is a user-defined class name.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: replace deprecated 'builtin' repo with pre-commit-hooks in pre-commit config

pre-commit 4.x no longer supports 'repo: builtin'. Merge those hooks into
the existing pre-commit-hooks repo entry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* style: apply pyupgrade formatting to docstring example

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: resolve pre-commit hook paths for monorepo git root

The poe-check and bandit hooks referenced paths relative to python/
but pre-commit runs hooks from the git root (monorepo root). Fix
poe-check entry to cd into python/ first, and update bandit config
path to python/pyproject.toml.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix pre-commit config paths for prek --cd python execution

Revert bandit config path from 'python/pyproject.toml' to 'pyproject.toml'
and poe-check entry from explicit 'cd python' wrapper to direct invocation,
since prek --cd python already sets the working directory to python/.

Also apply ruff formatting fixes to cosmos checkpoint storage files.

Fixes #4894

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: add builtins:getattr to checkpoint deserialization allowlist

Pickle uses builtins:getattr to reconstruct enum members (e.g.,
WorkflowMessage.type which is a MessageType enum). Without it in the
allowlist, checkpoint roundtrip tests fail with
WorkflowCheckpointException.

Fixes #4894

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4894: review comment fixes

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 00:04:17 +00:00
5e8fe0be1f Python: Stop emitting duplicate reasoning content from OpenAI response.reasoning_text.done and response.reasoning_summary_text.done events (#5162)
* Fix reasoning text done events duplicating streamed delta content (#5157)

The OpenAI Responses API sends both reasoning_text.delta (incremental
chunks) and reasoning_text.done (full accumulated text) events. The
chat client was emitting Content for both, causing ag-ui to append the
full done text onto already-accumulated delta text, producing
duplicated reasoning output.

Stop emitting Content for reasoning_text.done and
reasoning_summary_text.done events, matching how output_text.done is
already handled (not emitted). The deltas contain all the content;
the done event is redundant.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(openai): emit reasoning done content as fallback when no deltas observed (#5157)

Address PR review feedback:
- Track item_ids that received reasoning deltas via seen_reasoning_delta_item_ids set
- Emit content from done events only when no deltas were received for the
  item_id, preventing silent content loss on stream resumption
- Add comment documenting code_interpreter done event asymmetry
- Replace redundant ag-ui test with deduplication-focused test
- Add integration test for delta+done sequence in OpenAI chat client tests
- Add fallback path tests for done events without preceding deltas

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #5157: Python: [Bug]: "type": "response.reasoning_text.delta" and "response.reasoning_text.done" both get exposed as "text_reasoning"

* Fix AG-UI reasoning streaming to use proper Start/End pattern (#5157)

_emit_text_reasoning now follows the same streaming pattern as _emit_text:
- Emits ReasoningStartEvent/ReasoningMessageStartEvent only on the first
  delta for a given message_id
- Emits only ReasoningMessageContentEvent for subsequent deltas
- Defers ReasoningMessageEndEvent/ReasoningEndEvent until
  _close_reasoning_block is called (on content type switch or end-of-run)

This produces the correct protocol pattern:
  ReasoningStartEvent
    ReasoningMessageStartEvent
    ReasoningMessageContentEvent(delta1)
    ReasoningMessageContentEvent(delta2)
    ReasoningMessageEndEvent
  ReasoningEndEvent

Instead of wrapping every delta in a full Start→End sequence.

Backward compatibility is preserved: calling _emit_text_reasoning without
a flow argument still produces the full sequence per call.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix import ordering lint error in AG-UI test file (#5157)

Move inline import of TextMessageContentEvent to the top-level import
block and ensure alphabetical ordering to satisfy ruff I001 rule.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix mypy error: rename loop variable to avoid type conflict with WorkflowEvent

The 'event' variable was already typed as WorkflowEvent[Any] from the
async for loop at line 590. Reusing it in the _close_reasoning_block
loop (which returns list[BaseEvent]) caused an incompatible assignment
error. Renamed to 'reasoning_evt' to avoid the conflict.

Fixes #5162

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #5157: review comment fixes

* narrow test result reporting to explicit pytest JUnit XML

* Fix test args

* Fix pytest-results-action in merge workflow and remove committed test artifacts

Apply the same JUnit XML fix from python-tests.yml to python-merge-tests.yml:
add --junitxml=pytest.xml to all test commands and narrow the results action
path from ./python/**.xml to ./python/pytest.xml. Also remove accidentally
committed pytest.xml and python-coverage.xml and add them to .gitignore.

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 22:44:59 +00:00
Giles OdigweandGitHub 1dd828d255 CHANGELOG Update with V1.0.0 Release (#5069) 2026-04-09 22:39:01 +00:00
westeyandGitHub 8348584ac2 VerifySamples: Filter projects to net10 only (#5184) 2026-04-09 16:43:54 +00:00
westeyandGitHub 6d6cb840ae .NET: Improve resilience of verify-samples by building separately and improving evaluation instructions (#5151)
* Improve resilience of verify-samples by building separately and improving evaluation instructions

* Address PR comments

* Address PR comment
2026-04-09 11:25:00 +00:00
westeyandGitHub 79afda1a6c Samples fixes (#5169) 2026-04-09 08:46:20 +00:00
30a2bc3dcb Python: Add Cosmos DB NoSQL Checkpoint Storage for Python Workflows (#4916)
* Add CosmosCheckpointStorage for Python workflow checkpointing

Add native Cosmos DB NoSQL support for workflow checkpoint storage in the
Python agent-framework-azure-cosmos package, achieving parity with the
existing .NET CosmosCheckpointStore.

New files:
- _checkpoint_storage.py: CosmosCheckpointStorage implementing the
  CheckpointStorage protocol with 6 methods (save, load, list_checkpoints,
  delete, get_latest, list_checkpoint_ids)
- test_cosmos_checkpoint_storage.py: Unit and integration tests
- workflow_checkpointing.py: Sample demonstrating Cosmos DB-backed
  workflow checkpoint/resume

Auth support:
- Managed identity / RBAC via Azure credential objects
  (DefaultAzureCredential, ManagedIdentityCredential, etc.)
- Key-based auth via account key string or AZURE_COSMOS_KEY env var
- Pre-created CosmosClient or ContainerProxy

Key design decisions:
- Partition key: /workflow_name for efficient per-workflow queries
- Serialization: Reuses encode/decode_checkpoint_value for full Python
  object fidelity (hybrid JSON + pickle approach)
- Container auto-creation via create_container_if_not_exists

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Adding cosmos checkpointer

* Resolving comments

* Fixing builds

* Adding sample for history provider and checkpoint storage

* Resolving comments

* fixing builds

* Resolving comments

---------

Co-authored-by: Aayush Kataria <aayushkataria@Aayushs-MacBook-Pro-2.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-04-09 05:01:41 +00:00
Evan MattsonandGitHub a7a02c1abd Fix test compat for entity key validation (#5179) 2026-04-09 13:56:26 +09:00
7010dd7439 .NET: Support custom types in skill resource and script functions (#5152)
* .NET: Add JsonSerializerOptions support to programmatic skill APIs

Allow callers to pass custom JsonSerializerOptions when creating inline
resources and scripts via AgentInlineSkill, AgentClassSkill,
AgentInlineSkillResource, and AgentInlineSkillScript. A skill-level
default can be set on AgentInlineSkill and overridden per-resource/
script call.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/TestSkillTypes.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-08 09:55:27 +00:00
18d1ba3624 Python: Strip tools from FoundryAgent request when agent_reference is present (#5101)
_prepare_options() now removes tools, tool_choice, and parallel_tool_calls
from run_options after injecting agent_reference. The Foundry API rejects
requests containing both fields. FunctionTools are still invoked client-side
by the function invocation layer.

Fixes #5087

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-04-08 04:35:01 +00:00
Evan MattsonandGitHub e10d448ae2 Fix handoff workflow context management and improve AG-UI demo (#5136) 2026-04-08 04:08:24 +00:00
f94a75daa5 Python: Fix response_format crash on background polling with empty text (#5146)
* Guard against empty text in _parse_structured_response_value (#5145)

When using response_format with background=True (Responses API), polling
an in-progress response produces empty text. _parse_structured_response_value
unconditionally passed this to model_validate_json/json.loads, causing
ValidationError or JSONDecodeError.

Add an early return of None when text is empty, matching the existing
guard for response_format=None. This allows .value to safely return None
for in-progress background responses.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Fix `response_format` crash on background polling with empty text

Fixes #5145

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-07 22:26:03 +00:00
36cafe4e5a Python: Raise clear handler registration error for unresolved TypeVar annotations (#4944)
* Raise clear handler registration error for unresolved TypeVar (#4943)

Detect unresolved TypeVar in message parameter annotations during handler
registration in both _validate_handler_signature (Executor) and
_validate_function_signature (FunctionExecutor). Raises a ValueError with
an actionable message recommending @handler(input=..., output=...) or
@executor(input=..., output=...) instead of letting TypeVar leak through
to a confusing TypeCompatibilityError during workflow edge validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4943: reorder checks and harden function executor

- Move TypeVar check before validate_workflow_context_annotation in
  _executor.py so users see the more actionable error first
- Wrap get_type_hints in try/except in _function_executor.py matching
  the defensive pattern in _executor.py
- Repurpose duplicate test to cover bounded TypeVar rejection
- Add test_function_executor_allows_concrete_types for test symmetry

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Narrow get_type_hints except clause and add missing tests (#4943)

- Narrow `except Exception` to `except (NameError, AttributeError, RecursionError)`
  in both _executor.py and _function_executor.py so unexpected failures in
  get_type_hints are not silently swallowed.
- Add test_handler_unresolvable_annotation_raises to test_function_executor_future.py
  exercising the except branch of get_type_hints in the function executor path.
- Add test_function_executor_rejects_bounded_typevar_in_message_annotation to
  test_function_executor.py for parity with the Executor bounded TypeVar test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add error ordering test for TypeVar vs WorkflowContext priority (#4943)

Add test_handler_typevar_error_takes_priority_over_context_error to verify
that when a handler has both a TypeVar message and an unannotated ctx, the
TypeVar error is raised first (the more actionable issue).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Fix image content serialization sending null file_id to Foundry API

Omit file_id from input_image dict when not present instead of including
it as null, which Azure AI Foundry's stricter schema validation rejects.

* Python: Fix Foundry API rejecting rich content in function_call_output

Azure AI Foundry does not support list-format output in function_call_output
items. Add SUPPORTS_RICH_FUNCTION_OUTPUT flag (default True) to
RawOpenAIChatClient, set to False in RawFoundryChatClient so Foundry
falls back to string output for tool results with images/files.

Also omit file_id from input_image dicts when not set, since Foundry
rejects explicit nulls.

* Python: Surface rich tool content as user message when Foundry lacks support

When SUPPORTS_RICH_FUNCTION_OUTPUT is False, image/file items from tool
results are injected as a follow-up user message so the model can still
process the visual content via Foundry's supported user message format.

* Xfail Foundry image integration test for the meantime

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-07 16:59:17 +00:00
942cb04ccb .NET: Fix compaction chat history duplication bug (#5149)
* Fix chat history duplication bug

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-07 16:16:28 +00:00
westeyandGitHub e224f06e60 .NET: Update models used in dotnet samples to gpt-5.4-mini (#5080)
* Update models used in dotnet samples to gpt-5.4-mini

* Fix additional missed sample
2026-04-07 15:34:00 +00:00
Jacob AlberandGitHub 826d8db84c .NET: fix: Concurrent Workflow Sample (#5090)
* fix: Concurrent Workflow Sample

* Switch to using Azure AI Projects APIs
* Remove agent streaming outputs by changing emitEvents to false on TurnToken
* Disable forwarding input from agent host executors
* Make output format more legible

* refactor: Update Concurrent sample to use message delivery event callback
2026-04-07 14:28:21 +00:00
Roger BarretoandGitHub 4134c74060 Add CreateSessionAsync(conversationId) to FoundryAgent (#5144)
Adds a public CreateSessionAsync(string conversationId, CancellationToken)
method to FoundryAgent that delegates to the inner ChatClientAgent,
allowing users to create sessions with existing server-side conversation IDs.

Fixes #5138
2026-04-07 12:56:07 +00:00
SergeyMenshykhandGitHub 86b49d800e Fix and simplify ComputerUse sample (#5075)
* fix the computer use sample

* rollback changes to the search state enum

* address review comments

* address review comments
2026-04-07 12:29:31 +00:00
d73c06fa8c .NET: Align skill folder discovery with spec (#5078)
* add class-based skills

* address formating issues

* Remove generated filtered-unit.slnx and add to .gitignore

The filtered solution file is generated dynamically by
eng/scripts/New-FilteredSolution.ps1 during CI. Checking it in
risks it becoming stale and out-of-sync with the real solution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove generated filtered-unit.slnx and add to .gitignore

The filtered solution file is generated dynamically by
eng/scripts/New-FilteredSolution.ps1 during CI. Checking it in
risks it becoming stale and out-of-sync with the real solution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* discover scripts and resource from folders defined in spec

* Remove Step05 and Step06 DI skill samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* address review comments

* fix build error

* Fix mixed path separators in skill folder discovery on .NET Framework

Path.Combine with forward-slash folder names (e.g. "scripts/f1") produces
mixed separators on Windows, causing the StartsWith containment check to
fail against Path.GetFullPath-resolved file paths. Wrap in Path.GetFullPath
to canonicalize separators before the containment comparison.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* address comment

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-07 12:15:46 +00:00
090b88a956 Python: Adds sample documentation for two separate Neo4j context providers for retrieval and memory (#4010)
* Python: Adds sample documentation for two separate Neo4j context providers for retrieval and memory

* adding pypi links

* adding dotnot examples

* adding dotnot examples

* merge upstream samples

* fixing docs

* fix relative paths

---------

Co-authored-by: Ben Lackey <ben.lackey@neo4j.com>
2026-04-07 09:57:35 +00:00
746c7da216 Revise agent examples in README.md (#5067)
* Revise agent examples in README.md

Updated examples for creating agents using OpenAI and Azure AI, and updated Important notice

* Update README.md

Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>

* Update README.md

Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>

* Update README.md

Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>

* Update README.md

Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>

* Update README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update README.md

Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>

* Update README.md

Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>

* Update README.md

Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>

---------

Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-06 20:05:22 +00:00
Peter IbekweandGitHub d30103fee6 .NET: Fix input signal issue during checkpoint restoration (#5085)
* Improve workflow unit tests

* Update test name prefix for clarity.

* Update tests to surface any errors.

* fix check-point restore-time race in off-thread workflow event stream
2026-04-03 22:58:25 +00:00
Jacob AlberandGitHub 55ae57c0ed .NET: Add Message Delivery Callback Overloads to Executor (#5081)
* feat: Implement Executor Message Delivery Event callbacks

* fix: ResumeAsync does not run pending steps

* fix: address review comments
2026-04-03 21:19:15 +00:00
Jacob AlberandGitHub d284d96a9e fix: 04_MultiModelService sample (#5074)
- Change Bedrock to Google GenAI provider
- Fix use of OpenAI ("gpt-4o-mini" requires Responses API to use HostedWebSearchTool)
- Clean up output
2026-04-03 19:17:49 +00:00
Tao ChenGitHubTaoChenOSUcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
e94cfc6aef Python: Remove pre-release flag from agent-framework installation (#5082)
* Remove pre-release flag from agent-framework installation

* README: remove --pre from Python Quickstart pip install comment

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/c2444957-235e-43a1-9777-df9fdf12919b

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>
2026-04-03 16:47:23 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d1a81159de Bump Anthropic from 12.8.0 to 12.11.0 (#5055)
---
updated-dependencies:
- dependency-name: Anthropic
  dependency-version: 12.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 15:48:59 +00:00
Peter IbekweandGitHub 9f0dbe5f8d .NET: Improve workflow unit test coverage (#5072)
* Improve workflow unit tests

* Update test name prefix for clarity.

* Update tests to surface any errors.
2026-04-03 15:04:59 +00:00
3fc1d00026 .NET: skill as class (#5027)
* add class-based skills

* address formating issues

* Remove generated filtered-unit.slnx and add to .gitignore

The filtered solution file is generated dynamically by
eng/scripts/New-FilteredSolution.ps1 during CI. Checking it in
risks it becoming stale and out-of-sync with the real solution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove generated filtered-unit.slnx and add to .gitignore

The filtered solution file is generated dynamically by
eng/scripts/New-FilteredSolution.ps1 during CI. Checking it in
risks it becoming stale and out-of-sync with the real solution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* consolidate DI samples into one

* fix file encoding

* suppress compatibility warning

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-03 11:27:36 +00:00
westeyandGitHub e4defadc79 .NET: Add github actions workflow for verify-samples (#5034)
* Add github actions workflow for verify-samples

* Make workflow run as part of PR (for now)

* Update workflow to remove pr trigger

* Address PR comments
2026-04-03 09:58:24 +00:00
Jacob AlberandGitHub c798cb7a2e release: Mark Handoff Orchestrations Experimental (#5065) 2026-04-02 15:48:19 +00:00
Eduard van ValkenburgandGitHub 3446eb8d5d Python: [BREAKING] update to v1.0.0 (#5062)
* updates to final deprecated pieces and versions

* fix mypy

* fix readme links
2026-04-02 15:26:30 +00:00
5f06b68535 Python: handle streamed A2A update events (#4919)
* Python: handle streamed A2A update events

* Python: preserve terminal A2A artifacts during streaming

* Python: harden streamed A2A update event handling

* Python: simplify streamed A2A update guard

---------

Co-authored-by: sztoplover-bit <253473756+sztoplover-bit@users.noreply.github.com>
Co-authored-by: Giles Odigwe <79032838+giles17@users.noreply.github.com>
2026-04-02 15:20:47 +00:00
westeyandGitHub 524c0216e4 .NET: Update release versions (#5059)
* Update release versions

* Fix versioning issue

* Change ga wording to released

* Update suppression file

* Remove unecessary workflows suppression file

* Remove another unnecessary suppression file.

* Fix suppressions file
2026-04-02 14:54:28 +00:00
Jacob AlberandGitHub 281661e409 .NET: Remove timeout from InputWait in OffThread execution (#4996)
* fix: Remove Timeout from InputWait in StreamingRunEventStream

* fix: Race condition when the workflow executes to halt before TakeEventStream

* test: Make the OffThread Delay test more nimble

* fix: Remove slight window where runStatus could be stale
2026-04-02 14:35:25 +00:00
Roger BarretoandGitHub b0613a8ceb .NET: Bump Azure.AI.Projects to 2.0.0 GA (#5060)
* Bump Azure.AI.Projects to 2.0.0 GA

- Update Azure.AI.Projects from 2.0.0-beta.2 to 2.0.0 in CPM
- Update Azure.Identity from 1.19.0 to 1.20.0 (transitive dep)
- Update System.ClientModel from 1.9.0 to 1.10.0 (transitive dep)
- Rename types per Azure.AI.Projects.Agents 2.0.0 breaking changes:
  - AgentVersion -> ProjectsAgentVersion
  - AgentRecord -> ProjectsAgentRecord
  - AgentDefinition -> ProjectsAgentDefinition
  - AgentVersionCreationOptions -> ProjectsAgentVersionCreationOptions
  - PromptAgentDefinition -> DeclarativeAgentDefinition
  - AgentTool -> ProjectsAgentTool
  - AgentsClient -> AgentAdministrationClient
  - .Agents property -> .AgentAdministrationClient
- Add using Azure.AI.Projects.Memory namespace (types moved)
- Update AGENTS.md with BOM and output capture conventions

* Address PR review feedback

- Rename AIProjectClient parameter to aiProjectClient in AsChatClientAgent overloads
- Fix XML doc: ProjectsAgentTool namespace from Azure.AI.Projects.OpenAI to Azure.AI.Projects.Agents
- Rename test method to reflect DeclarativeAgentDefinition terminology
2026-04-02 14:02:29 +00:00
Jacob AlberandGitHub 79b38040e8 fix: Update Google.GenAI to verison compatible with M.E.AI 10.4.0+ (#5061) 2026-04-02 13:57:40 +00:00
a356a16568 .NET: Remove OpenAIAssistantClientExtensions class (#5058)
* Remove OpenAIAssistantClientExtensions class

Remove the deprecated OpenAI Assistants API extension methods, along with
their unit tests, integration tests, sample project, and related references.

- Delete OpenAIAssistantClientExtensions.cs (source)
- Delete OpenAIAssistantClientExtensionsTests.cs (unit + integration tests)
- Delete Agent_With_OpenAIAssistants sample project
- Remove sample from solution file, README, and verify-samples definitions
- Remove AIOpenAIAssistants diagnostic ID constant

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* add removed extension methods to the suppression file

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 13:51:41 +00:00
Eduard van ValkenburgandGitHub 95fd5ec658 Python: [BREAKING] Python: move Azure AI embeddings to Foundry (#5056)
* renamed AzureAIINferenceEmbeddings and lazy load azure-cosmos and env var rename

* updated coverage

* fix readme
2026-04-02 11:26:35 +00:00
chetantoshniwalGitHubchetantoshniwalcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
47d82911c0 Python: Fix server_tool_use input_json_delta handling and improve Anthropic samples (#5050)
* Fix server_tool_use input_json_delta handling and improve Anthropic samples

- Fix: Skip input_json_delta for server_tool_use content blocks in AnthropicClient streaming. Server-managed tools (e.g., skills with code interpreter) were producing Content.from_function_call(name='') entries that caused Anthropic API 400 errors on subsequent turns.

- Samples: Add dotenv loading and environment variable documentation to Anthropic Claude samples (MCP, permissions, session, shell, tools, URL, skills).

* Add regression test for server_tool_use + input_json_delta skip behavior

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/7c68dcb2-b577-4e36-b423-664b8fe3ac1d

Co-authored-by: chetantoshniwal <255221507+chetantoshniwal@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: chetantoshniwal <255221507+chetantoshniwal@users.noreply.github.com>
2026-04-02 09:43:59 +00:00
339e76d51f Python: Fix GitHubCopilotAgent to invoke context provider before_run/after_run hooks (#5013)
* Fix GitHubCopilotAgent not calling context provider hooks (#3984)

GitHubCopilotAgent accepted context_providers in its constructor but
never called before_run()/after_run() on them in _run_impl() or
_stream_updates(), silently ignoring all context providers.

Add _run_before_providers() helper to create SessionContext and invoke
before_run on each provider. Both _run_impl() and _stream_updates() now
run the full provider lifecycle: before_run before sending the prompt
(with provider instructions prepended) and after_run after receiving the
response. This follows the same pattern used by A2AAgent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Fix GitHubCopilotAgent to invoke context provider before_run/after_run hooks

Fixes #3984

* fix(#3984): address review feedback for context provider integration

- Build prompt from session_context.get_messages(include_input=True) so
  provider-injected context_messages are included in both non-streaming
  and streaming paths (review comments #1, #2)
- Preserve timeout in opts (use get instead of pop) so providers can
  observe it via context.options (review comment #3)
- Eliminate streaming double-buffer: move after_run invocation to a
  ResponseStream result_hook (matching Agent class pattern) instead of
  maintaining a separate updates list in the generator (review comment #4)
- Improve _run_before_providers docstring

Add tests for:
- Context messages included in prompt (non-streaming + streaming)
- Error path: after_run NOT called when send_and_wait/streaming raises
- Multiple providers: forward before_run, reverse after_run ordering
- BaseHistoryProvider with load_messages=False is skipped
- Streaming after_run response contains aggregated updates
- Streaming with no updates still sets empty response
- Timeout preserved in session context options for providers

Note: _run_before_providers remains on GitHubCopilotAgent for now. A
follow-up PR should extract it to BaseAgent so subclasses can reuse it
without duplicating the provider iteration logic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #3984: Python: [Bug]: GitHubCopilotAgent Memory Example

* refactor(#3984): promote _run_before_providers to BaseAgent

Move _run_before_providers from GitHubCopilotAgent into BaseAgent,
mirroring the existing _run_after_providers helper. Agent's
_prepare_session_and_messages now delegates to the shared base method,
eliminating the near-duplicate provider iteration logic that could
drift as the provider contract evolves.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #3984: Python: [Bug]: GitHubCopilotAgent Memory Example

* revert: keep _run_before_providers in GitHubCopilotAgent only

Undo the promotion of _run_before_providers to BaseAgent. The method
stays in GitHubCopilotAgent where it is needed, and _agents.py
retains its original inline provider iteration in RawAgent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: replace deprecated BaseContextProvider/BaseHistoryProvider with ContextProvider/HistoryProvider

Update imports and usages in GitHubCopilotAgent and its tests to use
the new non-deprecated class names from the core package.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address review feedback - reorder providers before session, wrap streaming after_run in try/except, assert after_run on skipped HistoryProvider

- Move _run_before_providers before _get_or_create_session so provider
  contributions can affect session configuration
- Wrap _run_after_providers in try/except in streaming _after_run_hook
  to prevent provider errors from replacing successful responses
- Add after_run assertion to test_history_provider_skip_when_load_messages_false

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 09:43:02 +00:00
Tao ChenandGitHub 62595b233f [BREAKING] Python: Refactor workflows kwargs (#5010)
* Refactor workflows kwargs usage

* Update sample

* Add tests

* Update samples

* Fix formatting

* Comments

* Comments 2

* Comments 3

* Fix test and typing
2026-04-02 09:40:39 +00:00
CopilotGitHubsphenrycopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Shawn Henry
fd253c0b0e Python: Move workflow-samples and agent-samples under declarative-agents directory (#5011)
* Move workflow-samples and agent-samples under declarative-agents and update all references

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/f70f7d19-9256-4eec-b7db-28007d74440c

Co-authored-by: sphenry <6749825+sphenry@users.noreply.github.com>

* Fix relative paths in README files inside moved directories

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/f70f7d19-9256-4eec-b7db-28007d74440c

Co-authored-by: sphenry <6749825+sphenry@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sphenry <6749825+sphenry@users.noreply.github.com>
Co-authored-by: Shawn Henry <shahen@microsoft.com>
2026-04-02 09:34:33 +00:00
hashwnathandGitHub 7e8e9e3074 Python: Fix duplicate system message from instructions (#5049) (#5051)
Add deduplication to `prepend_instructions_to_messages()` to skip
instructions that are already present as leading messages with the
same role and text. This prevents duplicate system messages when
instructions are injected by multiple layers (e.g. Agent + chat client).

Fixes #5049
2026-04-02 09:20:50 +00:00
CopilotGitHubTaoChenOSUcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
7607acd009 Python: Add experimental decorator to all Evals pieces (#5040)
* Initial plan

* feat(python): add experimental decorator to all Evals pieces

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/99d71249-a5d6-4977-a5b5-6ffe0a3be2bc

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>
2026-04-02 09:18:35 +00:00
Tao ChenandGitHub 3d87cec304 Python: Fix SK migration samples (#5047)
* Fix SK migration samples

* Fix env vars for SK

* Hard code model for sheel tool samples
2026-04-02 08:40:34 +00:00
Roger BarretoandGitHub 628bb1af48 .NET: Rename Microsoft.Agents.AI.AzureAI to Microsoft.Agents.AI.Foundry and consolidate FoundryMemory (#5042)
* Update Foundry Responses as ChatClientAgent

* Migrate obsolete AzureAI integration tests to versioned agent pattern

Replace obsolete CreateAIAgentAsync/GetAIAgentAsync calls with
Agents.CreateAgentVersionAsync() + AsAIAgent(AgentVersion) in all
AzureAI integration tests.

- Rename AIProjectClient* test files to FoundryVersionedAgent*
- Register AIFunction tools in PromptAgentDefinition.Tools for
  server-side visibility via AsOpenAIResponseTool()
- Skip structured output tests (AzureAIProjectChatClient clears
  ResponseFormat for versioned agents)
- Remove all [Obsolete] attributes and #pragma warning disable CS0618

* Merge FoundryMemory package into AzureAI under Memory/ folder

Move all FoundryMemory source, unit tests, and integration tests into
the Microsoft.Agents.AI.AzureAI package. Change namespace from
Microsoft.Agents.AI.FoundryMemory to Microsoft.Agents.AI.AzureAI.

- Add [Experimental] to FoundryMemoryProviderOptions and Scope
- Rename internal AIProjectClientExtensions to MemoryStoreExtensions
- Update AzureAI .csproj with Compliance.Abstractions, Redaction
- Remove FoundryMemory from solution and release filter
- Update sample to reference AzureAI instead of FoundryMemory
- Delete old Microsoft.Agents.AI.FoundryMemory project and tests

* Add EnsureMemoryStoreCreatedAsync and memory existence checks to integration tests

- Ensure memory store is created before testing memory operations
- Add AZURE_AI_EMBEDDING_DEPLOYMENT_NAME config setting
- Assert memories exist in store via SearchMemoriesAsync before cleanup
- Verify scope isolation with direct memory store queries

* Fix and rename AzureAI unit tests for RAPI vs Versioned clarity

- Rename AsAIAgentAsync_* to AsAIAgent_* (drop Async from method group)
- Add _Rapi_ prefix to non-versioned (Responses API) tests
- Add _Versioned_ prefix to versioned agent tests where needed
- Fix RAPI tests: assert GetService<AIProjectClient>() is null
- Fix Versioned tests: assert IsType<FoundryAgent> and
  GetService<AIProjectClient>() returns the client instance
- Fix UserAgent header tests: proper HTTP handler routing
- Fix ChatClient_UsesDefaultConversationIdAsync test setup
- All 153 unit tests pass with 0 failures

* Rename Microsoft.Agents.AI.AzureAI to Microsoft.Agents.AI.Foundry

Rename the project, namespace, folder, and all references from
Microsoft.Agents.AI.AzureAI to Microsoft.Agents.AI.Foundry.
Also rename Workflows.Declarative.AzureAI to .Foundry.

- Rename src, unit test, integration test, and workflow folders
- Update namespaces in all source and test .cs files
- Update ProjectReferences in ~47 sample and test .csproj files
- Update solution files (.slnx, .slnf)
- Update sample using statements
- Update READMEs, SKILL.md, ADRs in docs/
- Disable package validation baseline for renamed packages
- Fix UTF-8 BOM encoding on all affected .cs files
- AzureAI.Persistent left completely unchanged

* Fix format: remove ImplicitUsings, add explicit usings, fix BOM encoding

- Remove ImplicitUsings=enable from Foundry csproj to resolve IDE0005
  on shared ReplacingRedactor.cs
- Add explicit System usings to all source files that relied on them
- Sort usings alphabetically per editorconfig rules
- Fix UTF-8 BOM on 12 sample Program.cs files
- Rename Azure AI Foundry Agents to Microsoft Foundry Agents in docs
2026-04-02 01:25:24 +00:00
6f6ee61834 Python: Fix broken samples and add missing READMEs (#5038)
* Python: Fix broken samples and add missing READMEs

- simple_context_provider: move instructions kwarg into options dict
- suspend_resume_session: use OpenAIChatCompletionClient for in-memory demo
- foundry_chat_client_with_hosted_mcp: move store kwarg into options dict
- Add README.md for context_providers and conversations sample folders

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Fix additional sample issues in context_providers

- mem0_basic: send preferences query before sleep so Mem0 can learn them,
  print result from new session recall
- mem0_sessions: add session for multi-turn conversation in agent-scoped
  example, remove user_id from agent-scoped provider (Mem0 API stores
  memories without user_id when agent_id is provided), use single message
  for storing preferences
- redis_basics: print retrieved context messages instead of raw object
- redis_sessions: add missing load_dotenv() call
- redis_basics/redis_sessions: fix docstrings referencing wrong client type
- azure_redis_conversation: replace duplicate copyright with load_dotenv()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Fix broken link in declarative README

openai_responses_agent.py was renamed to openai_agent.py

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-01 21:35:16 +00:00
westeyandGitHub 15e435b472 Fix telemetry sample bug (#5037) 2026-04-01 19:30:45 +00:00
Eduard van ValkenburgandGitHub 519bb0cb2b Python: updated declarative samples and handling of non-pydantic response formats (#5022)
* updated declarative samples and handling of non-pydantic response formats

* fixed from comments

* update docstring
2026-04-01 19:16:00 +00:00
6acab3d1d6 Python: [BREAKING] Standardize model selection on model (#4999)
* Refactor Anthropic model option and provider clients

Rename the Anthropic client model option from model_id to model, add provider-specific Anthropic wrappers for Foundry, Bedrock, and Vertex, and expose them through the Anthropic, Foundry, Amazon, and Google namespaces. Update core option handling, docs, samples, and tests accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Anthropic skills sample typing

Cast the Anthropic beta client to Any in the skills sample so the pre-commit sample pyright check no longer fails on beta skills and files endpoints that are not exposed by the current SDK stubs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* undo sample mypy

* Retry CI after transient external failures

Retrigger PR validation after an unrelated Copilot review workflow SAML failure and a transient external tau2 git fetch failure in the Windows Python test setup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback on model option merging

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Anthropic compatibility review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* moved all to `model`

* fixes for azure ai search

* Python: standardize remaining sample env var names

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: fix foundry-local pyright compatibility

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* updated env vars in cicd

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-01 19:00:18 +00:00
Tao ChenGitHubTaoChenOSUcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
95550dd0dc Python: Enforce Foundry package unit test coverage (#5036)
* Enforce Foundry package unit test coverage

* Sort ENFORCED_TARGETS alphabetically in python-check-coverage.py

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/ed0b81ed-c267-4ee0-9655-56c4b3066fad

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>
2026-04-01 17:37:27 +00:00
86f8efc8ff Python: Fix observability samples (#5016)
* Fix observability samples

* Update python/samples/02-agents/observability/configure_otel_providers_with_parameters.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/samples/02-agents/observability/agent_observability.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update python/samples/02-agents/observability/agent_observability.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 17:12:03 +00:00
b065a4ce51 Python: [BREAKING] update context provider APIs, middleware, and per-service-call history persistence (#4992)
* Rename provider base APIs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Allow provider-added chat and function middleware

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Simulate service-stored history per model call

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix typing regressions in CI

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix response ID suppression review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Rename per-service-call history persistence APIs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address context persistence review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Stabilize markdown sample docs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Persist service continuation state per call

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-01 16:13:11 +00:00
Peter IbekweandGitHub 38de991481 .NET: Fix RequestInfoEvent lost when resuming workflow from checkpoint (#4955)
* Fix RequestInfoEvent lost when resuming workflow from checkpoint

* Fix streaming run double disposal in tests and lockstep republishing before Started event is emitted.

* Fix bug to remove messages after sending to avoid losing messages on send failure.

* Fix declarative test harness
2026-04-01 15:38:48 +00:00
25696a72dc .NET: Replace Azure Foundry/Azure AI Foundry with Microsoft Foundry in .NET samples (#5032)
* Replace Azure Foundry/Azure AI Foundry with Microsoft Foundry in samples

Update all .cs, .md, and .yaml files in dotnet/samples/ to use
'Microsoft Foundry' instead of 'Azure Foundry' and 'Azure AI Foundry'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/samples/02-agents/Agents/Agent_Step15_DeepResearch/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/samples/05-end-to-end/A2AClientServer/README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/samples/02-agents/Agents/Agent_Step15_DeepResearch/README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/samples/03-workflows/Agents/FoundryAgent/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix grammar: 'an Microsoft' -> 'a Microsoft', 'agents ids' -> 'agent IDs'

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-01 15:18:33 +00:00
Eduard van ValkenburgandGitHub 2cb78ea12e fix and unify devui samples (#5025) 2026-04-01 13:47:20 +00:00
Eduard van ValkenburgandGitHub cee0a458fe Python: fixed middleware samples (#5026)
* fixed samples

* small update to explanation

* add snippet fix on root readme
2026-04-01 13:40:27 +00:00
Eduard van ValkenburgandGitHub 4b9856e66f Python: updated azure ai inference sample (#5028)
* updated azure ai inference sample

* openai multimodel fix

* update language
2026-04-01 13:35:56 +00:00
westeyandGitHub acaadc9c45 .NET: Add a verify-samples tool and skill (#5005)
* Add a verify-samples tool and skill

* Address PR comments

* Move verify-samples to eng folder and improve definitions
2026-04-01 10:34:41 +00:00
Christian GlessnerandGitHub 34329840e1 Add Neo4j GraphRAG samples (#4994)
* Add Neo4j GraphRAG samples

* Fix sample CI issues

* Address sample review feedback

* Move Neo4j Python sample to end-to-end

* Make Neo4j GraphRAG sample self-contained

* Remove unused central package versions
2026-04-01 10:23:04 +00:00
Eduard van ValkenburgandGitHub 2a8c3e2dcf fixes to azure ai search init, samples (#5021) 2026-04-01 09:59:52 +00:00
Tao ChenandGitHub e43fc8ccec Python: Fix migration samples (#5015)
* Fix migration samples

* Fix migration samples 2

* Fix formatting

* Comments
2026-04-01 06:32:30 +00:00
d992febe9b Python: Fix agent_with_hosted_mcp sample to use Foundry client for MCP tools (#4867)
* Fix agent_with_hosted_mcp sample to use AzureOpenAIResponsesClient (#4861)

The agent_with_hosted_mcp sample used AzureOpenAIChatClient with an MCP tool
dict, but the Chat Completions API only supports 'function' and 'custom' tool
types, not 'mcp'. This caused a 400 error at runtime.

Switch the sample to AzureOpenAIResponsesClient which natively supports MCP
tools via the Responses API. Use get_mcp_tool() to construct the tool config.

Changes:
- main.py: Replace AzureOpenAIChatClient with AzureOpenAIResponsesClient
- requirements.txt: Update azure-ai-agentserver-agentframework to 1.0.0b16
  and use agent-framework-azure-ai package
- agent.yaml: Use AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME env var
- Add regression test documenting chat client MCP tool passthrough behavior

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Fix agent_with_hosted_mcp sample to use Responses API client for MCP tools

Fixes #4861

* Remove REPRODUCTION_REPORT.md investigation artifact (#4861)

Remove the reproduction report markdown file from the test directory.
Investigation notes belong in the GitHub issue or PR description,
not as committed files in the source tree. The regression test in
test_openai_chat_client.py already provides automated verification.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add MCP tool API rejection regression test (#4861)

Add test_mcp_tool_dict_causes_api_rejection to verify that MCP tool
dicts passed through to the Chat Completions API result in a clear
ChatClientException rather than being silently dropped. This completes
the regression test coverage requested in code review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* small fix

* Revert deletion of dotnet local.settings.json files

Restore the two local.settings.json files that were accidentally deleted in this PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 22:04:54 +00:00
651e317907 Python: Fix _add_text_reasoning_content to preserve id during coalescing (#4862)
* Fix _add_text_reasoning_content dropping id during coalescing (#4852)

Preserve the id field (rs_* identifier) when coalescing text_reasoning
Content objects by passing id=self.id or other.id to the Content
constructor. This fixes the encrypted reasoning round-trip where the
missing id prevented _prepare_content_for_openai from including it in
the serialized reasoning item.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Fix `_add_text_reasoning_content` to preserve `id` during coalescing

Fixes #4852

* Raise AdditionItemMismatch on conflicting text_reasoning ids (#4852)

Detect when both operands have different non-empty ids during
text_reasoning Content coalescing and raise AdditionItemMismatch
instead of silently keeping one. This prevents mis-associating
encrypted_content during round-trips.

Also adds tests for conflicting ids and the neither-has-id edge case.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4852: Python: [Bug]: Content._add_text_reasoning_content drops id during coalescing, breaking encrypted reasoning round-trip

* test fix

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 21:58:02 +00:00
1e527a328c Python: Remove unsupported memory scoping params from mem0/redis samples and docs (#4367)
* Python: Remove unsupported memory scoping params from samples and docs

Fixes #4353

The `Mem0ContextProvider` and `RedisContextProvider` no longer support
`thread_id` or `scope_to_per_operation_thread_id` parameters. This commit
updates the affected samples and READMEs to use only the currently
supported API (`user_id`, `agent_id`, `application_id`).

Changes:
- mem0_sessions.py: Remove `thread_id` and
  `scope_to_per_operation_thread_id` from examples 1 and 2, rewrite to
  demonstrate user-scoped and agent-scoped memory patterns
- redis_sessions.py: Update module docstring to remove references to
  removed thread scoping params
- mem0/README.md: Update Memory Scoping docs to reflect current API
- redis/README.md: Remove `thread_id` and
  `scope_to_per_operation_thread_id` references from docs

* Address Copilot review: rename thread_scope functions, fix docstring

- Rename `example_global_thread_scope` -> `example_global_memory_scope`
- Rename `example_per_operation_thread_scope` -> `example_agent_scoped_memory`
- Update example 2 docstring to mention `application_id` alongside
  `user_id` and `agent_id` since it's set in the provider config
- Update module docstring scenario 2 to include `application_id`

* fix: rebase onto main, address giles17 review feedback

- Resolve merge conflicts by rebasing all 4 original files onto current main
- Address giles17's agent review suggestions:
  - mem0_basic.py: update comment to remove thread_id from scoping list
  - mem0_oss.py: update comment to remove thread_id from scoping list
  - redis_sessions.py: rename Example 2 from "Agent-Scoped Memory" to
    "Hybrid Vector Search" to accurately describe what it demonstrates
  - redis/README.md: update Example 2 description to match renamed example

---------

Co-authored-by: Tao Chen <taochen@microsoft.com>
Co-authored-by: Giles Odigwe <79032838+giles17@users.noreply.github.com>
2026-03-31 21:57:23 +00:00
3a49b1d6dd Python: [BREAKING] Remove deprecated Python OpenAI/Azure AI surfaces (#4990)
* [BREAKING] Remove deprecated Python OpenAI/Azure AI surfaces

Also clean up follow-on docs, environment guidance, package metadata, and lab test stability.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix deleted semantic-kernel sample links

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* improve foundry language

* Fix A2A Foundry sample regression

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 20:36:21 +00:00
a5eacbbe65 Python: Add Python A2A agent-as-function-tools sample (#4889)
* Add Python A2A agent-as-function-tools sample

Port of the .NET A2AAgent_AsFunctionTools sample to Python.
Resolves a remote A2A agent card, converts each skill to a
FunctionTool via as_tool(), and registers them with a host agent
using AzureOpenAIResponsesClient.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Sanitize A2A skill names before passing to as_tool()

as_tool() only auto-sanitizes when name is omitted. Since we pass
skill.name explicitly, we need to strip special characters ourselves.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 20:00:40 +00:00
55b6e7a9f4 Python: Add Python feature lifecycle decorators for released APIs (#4975)
* Add Python feature lifecycle decorators

Introduce reusable experimental and release-candidate decorators for released packages, migrate the Skills APIs to the new staged metadata and warning system, and add lifecycle guidance plus samples.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Python CI follow-ups

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Preserve protocol runtime checks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 19:40:08 +00:00
9c9d81d8b6 .NET: Improve README: architecture overview, troubleshooting, and sample links (#5002)
* Fix README issues: simplify Azure quickstart, add missing sample links, fix typo

- Replace BearerTokenPolicy Azure snippet with simpler AzureOpenAIClient + DefaultAzureCredential pattern
- Add missing sample links for Python (04-hosting, 05-end-to-end) and .NET (01-get-started, 04-hosting, 05-end-to-end)
- Fix 'infererence' typo in dotnet/samples/README.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add architecture overview and troubleshooting sections to README

- Add ASCII architecture diagram showing AIAgent and Workflow pipelines
- Add agent-vs-workflow decision table with 8 common scenarios
- Add troubleshooting section for authentication issues and environment variables
- Fix 'infererence' typo in dotnet/samples/README.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix architecture diagram alignment

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix diagram

* Update architecture diagram and rename Azure AI Foundry to Microsoft Foundry

- Add A2AAgent and Skills to the architecture diagram
- Rename Azure AI Foundry references to Microsoft Foundry
- Add A2AAgent to agent type descriptions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* adderss comments

* address PR review comments

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 18:44:27 +00:00
westeyandGitHub 47a8a305d2 Fix environment variable set statement in py DEV_SETUP (#5006) 2026-03-31 18:34:04 +00:00
westeyandGitHub 6e7254bba7 .NET: [BREAKING] Rename from ServiceStoredSimulatingChatClient to PerServiceCallChatHistoryPersistingChatClient (#4993)
* Rename from ServiceStoredSimulatingChatClient to PerServiceCallChatHistoryPersistingChatClient

* Address PR comment
2026-03-31 17:32:05 +00:00
9c57680f00 Python: Add header_provider to Streamable HTTP MCP servers (#4849)
* Python: Add header_provider to MCPStreamableHTTPTool (#4808)

Add a header_provider callback parameter to MCPStreamableHTTPTool that
enables injecting dynamic per-request HTTP headers from runtime kwargs
(originating from FunctionInvocationContext.kwargs set in agent middleware).

The implementation uses contextvars and httpx event hooks to ensure headers
are task-local and safe for concurrent tool calls:

- header_provider receives the runtime kwargs dict and returns headers
- call_tool sets a ContextVar before delegating to MCPTool.call_tool
- An httpx request event hook reads from the ContextVar and injects headers

Example usage:
    mcp_tool = MCPStreamableHTTPTool(
        name="web-api",
        url="https://api.example.com/mcp",
        header_provider=lambda kwargs: {
            "X-Auth-Token": kwargs.get("auth_token", ""),
        },
    )

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4808: Python: [Bug]: Unable to pass AgentContext to MCPStreamableHTTPTool

* Add test for header_provider via FunctionTool.invoke with FunctionInvocationContext

Addresses PR review comment: exercises the full pipeline from
FunctionInvocationContext.kwargs through FunctionTool.invoke to
MCPStreamableHTTPTool.call_tool and header_provider, rather than
testing call_tool in isolation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4808: review comment fixes

* Fix streamable MCP transport defaults

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Azure AI test client mocks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix MCP runtime kwarg regressions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Stabilize MCP tool runtime kwargs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Use context kwargs in MCP wrappers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* updated mcp samples

* fix link

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 17:23:49 +00:00
7c2dae8855 Python: Fix sample bugs: incorrect API params, wrong client types, and invalid options (#4983)
* Fix sample bugs: incorrect API params, wrong client types, and invalid options

- typed_options.py: Fix AnthropicClient model->model_id, wrap raw strings in Message objects for get_response(), fix reasoning_effort->reasoning dict, fix budget_tokens minimum (1024), use OpenAIChatClient not FoundryChatClient, remove unused import

- client_reasoning.py: Fix deprecated model_id to model param

- client_with_hosted_mcp.py: Remove invalid store=True kwarg from Agent.run()

- code_defined_skill.py: Fix precision kwarg to use function_invocation_kwargs

- Various other samples: Fix deprecated API usage and incorrect params

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review comments

- client_with_hosted_mcp.py: Fix remaining store=True kwarg on line 68 to use options dict

- client_with_session.py: Change store=True to store=False to match in-memory persistence demo intent

- typed_options.py: Remove non-existent import and model key from docstring example

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* new sample fixes

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 16:58:51 +00:00
Peter IbekweandGitHub 3d09337446 Update project name from 'Semantic Kernel' to 'Agent Framework' (#5001) 2026-03-31 16:52:44 +00:00
3c727b5b71 Improve CONTRIBUTING.md with dev setup links and docs guidance (#5000)
* Improve CONTRIBUTING.md with dev setup links and docs guidance

- Consolidate Development Scripts into a Development Setup section with
  quick links to language-specific dev guides and coding standards
- Add Python build/test/lint commands alongside existing .NET commands
- Add Documentation Contributions section with link checker, writing
  guidelines, and style guidance

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Use directory note for .NET commands, matching Python style

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Split test commands into unit vs. integration for both Python and .NET

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove Documentation Contributions section

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 16:42:52 +00:00
35adfdb318 Python: Foundry Evals integration for Python (#4750)
* Foundry Evals integration for Python

Merged and refactored eval module per Eduard's PR review:

- Merge _eval.py + _local_eval.py into single _evaluation.py
- Convert EvalItem from dataclass to regular class
- Rename to_dict() to to_eval_data()
- Convert _AgentEvalData to TypedDict
- Simplify check system: unified async pattern with isawaitable
- Parallelize checks and evaluators with asyncio.gather
- Add all/any mode to tool_called_check
- Fix bool(passed) truthy bug in _coerce_result
- Remove deprecated function_evaluator/async_function_evaluator aliases
- Remove _MinimalAgent, tighten evaluate_agent signature
- Set self.name in __init__ (LocalEvaluator, FoundryEvals)
- Limit FoundryEvals to AsyncOpenAI only
- Type project_client as AIProjectClient
- Remove NotImplementedError continuous eval code
- Add evaluation samples in 02-agents/ and 03-workflows/
- Update all imports and tests (167 passing)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: resolve mypy redundant-cast errors while keeping pyright happy

Use cast(list[Any], x) with type: ignore[redundant-cast] comments to
satisfy both mypy (which considers casting Any redundant) and pyright
strict mode (which needs explicit casts to narrow Unknown types).

Also fix evaluator decorator check_name type annotation to be
explicitly str, resolving mypy str|Any|None mismatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: CI failures — pyupgrade, evaluator overloads, sample API, reset attr

- Apply pyupgrade: Sequence from collections.abc, remove forward-ref quotes
- Add @overload signatures to evaluator() for proper @evaluator usage
- Fix evaluate_workflow sample to use WorkflowBuilder(start_executor=) API
- Fix _workflow.py executor.reset() to use getattr pattern for pyright
- Remove unused EvalResults forward-ref string in default_factory lambda

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: skip gRPC-dependent observability test

The test_configure_otel_providers_with_env_file_and_vs_code_port test
triggers gRPC OTLP exporter creation, but the grpc dependency is
optional and not installed by default. Add skipif decorator matching
the pattern used by all other gRPC exporter tests in the same file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: add nosec B101 for bandit assert check

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* style: align eval samples with repo conventions

- Move module docstrings before imports (after copyright header)
- Add -> None return type to all main() and helper functions
- Fix line-too-long in multiturn sample conversation data
- Add Workflow import for typed return in all_patterns_sample

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review feedback: async fixes, sample bugs, deprecation warnings

- Simplify _ensure_async_result to direct await (async-only clients)
- Replace get_event_loop() with get_running_loop()
- Narrow _fetch_output_items exception handling to specific types
- Add warning log when _filter_tool_evaluators falls back to defaults
- Add DeprecationWarning to options alias in Agent.__init__
- Add DeprecationWarning to evaluate_response()
- Rename raw key to _raw_arguments in convert_message fallback
- Fix evaluate_agent_sample.py: replace evals.select() with FoundryEvals()
- Fix evaluate_multiturn_sample.py: use Message/Content/FunctionTool types
- Fix evaluate_workflow_sample.py: replace evals.select() with FoundryEvals()
- Update test mocks to use AsyncMock for awaited API calls

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add test coverage for review feedback items

- Add num_repetitions=2 positive test verifying 2×items and 4 agent calls
- Add _poll_eval_run tests: timeout, failed, and canceled paths
- Add evaluate_traces tests: validation error, response_ids path, trace_ids path
- Add evaluate_foundry_target happy-path test with target/query verification

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix ruff ISC004 lint error and apply formatter

- Wrap implicit string concatenation in parens in evaluate_multiturn_sample.py
- Apply ruff formatter to 6 other files with minor formatting drift

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove core type changes (extracted to fix/workflow-stale-session branch)

Reverts changes to _agents.py, _agent_executor.py, and _workflow.py
back to upstream/main. These fixes are now in a separate PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review round 2: bugs, tests, and architecture

Code fixes:
- Fix _normalize_queries inverted condition (single query now replicates
  to match expected_count)
- Fix substring match bug: 'end' in 'backend' matched; use exact set
  lookup for executor ID filtering
- Fix used_available_tools sample: tool_definitions→tools param, use
  FunctionTool attribute access instead of dict .get()
- Add None-check in _resolve_openai_client for misconfigured project
- Add Returns section to evaluate_workflow docstring
- Cache inspect.signature in @evaluator wrapper (avoid per-item reflection)

Architecture:
- Extract _evaluate_via_responses as module-level helper; evaluate_traces
  now calls it directly instead of creating a FoundryEvals instance
- Move Foundry-specific typed-content conversion out of core to_eval_data;
  core now returns plain role/content dicts, FoundryEvals applies
  AgentEvalConverter in _evaluate_via_dataset

Tests:
- evaluate_response() deprecation warning emission and delegation
- num_repetitions > 1 with expected_output and expected_tool_calls
- Mock output_items.list in test_evaluate_calls_evals_api
- Update to_eval_data assertions for plain-dict format
- Unknown param error now raised at @evaluator decoration time

Skipped (separate PR): executor reset loop, xfail removal, options alias

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix CI: revert test_full_conversation, fix pyright errors

- Revert test_full_conversation.py to upstream/main (the session
  preservation test was incorrectly changed to assert clearing)
- Fix pyright reportUnnecessaryComparison on get_openai_client() None
  check by adding ignore comment
- Fix pyright reportPrivateUsage: add public EvalItem.split_messages()
  method and use it in FoundryEvals._evaluate_via_dataset instead of
  accessing private _split_conversation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review round 3: reliability, test gaps, cleanup

- Add try/except guard for non-numeric score in _coerce_result
- Add poll_interval minimum bound (0.1s) to prevent tight loops
- Add runtime async client check in _resolve_openai_client
- Remove _ensure_async_result wrapper (10 call sites → direct await)
- Better error message when queries provided without agent
- Import-time asserts for evaluator set consistency
- Remove 28 redundant @pytest.mark.asyncio decorators
- Add doc note about _raw_arguments sensitive data
- Tests: tool_called_check mode=any, _normalize_queries branches,
  _extract_result_counts paths, _extract_per_evaluator, bare check
  via evaluate_agent, output_items assertion, modulo wrapping,
  async client check, queries-without-agent error

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix CI: ruff S101 assert, pyright and mypy arg-type errors

- Replace module-level assert with if/raise for evaluator set
  consistency checks (ruff S101 disallows bare assert)
- Add type: ignore[arg-type] and pyright: ignore[reportArgumentType]
  on OpenAI SDK evals API calls that pass dicts where typed params
  are expected (SDK accepts dicts at runtime)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review round 4: bugs, reliability, test fixes

- Fix all_passed ignoring parent result_counts when sub_results present
- Fix _extract_tool_calls: parse string arguments via json.loads before
  falling back to None (real LLM responses use string arguments)
- Sanitize _raw_arguments to '[unparseable]' to avoid leaking sensitive
  tool-call data to external evaluation services
- Add NOTE comment on to_eval_data message serialization dropping
  non-text content (tool calls, results)
- Eliminate double conversation split in _evaluate_via_dataset: build
  JSONL dicts directly from split_messages + AgentEvalConverter
- Raise poll_interval floor from 0.1s to 1.0s to prevent rate-limit
  exhaustion
- Fix MagicMock(name=...) bug in test: sets display name not .name attr
- Fix mock_output_item.sample: use MagicMock object instead of dict so
  _fetch_output_items exercises error/usage/input/output extraction

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review round 5: reliability, docs, test coverage

Code fixes:
- Move import-time RuntimeError checks to unit tests (avoids breaking
  imports for all users on developer set-drift mistake)
- _filter_tool_evaluators now raises ValueError when all evaluators
  require tools but no items have tools (was silently substituting)
- Add poll_interval upper bound (60s) to prevent single-iteration sleep
- Log exc_info=True in _fetch_output_items for debugging API changes
- Fix evaluate() docstring: remove claim about Responses API optimization
- Validate target dict has 'type' key in evaluate_foundry_target
- Document to_eval_data() limitation: non-text content is omitted

Tests:
- TestEvaluatorSetConsistency: verify _AGENT/_TOOL subsets of _BUILTIN
- TestEvaluateTracesAgentId: agent_id-only path with lookback_hours
- TestFilterToolEvaluatorsRaises: ValueError on all-tool no-items
- TestEvaluateFoundryTargetValidation: target without 'type' key
- Assert items==[] on failed/canceled poll results
- Mock output_items.list in response_ids test for full flow
- TestAllPassedSubResults: result_counts=None + sub_results delegation
  and parent failures override sub_results
- TestBuildOverallItemEmpty: empty workflow outputs returns None

Skipped r5-07 (_raw_arguments length hint): marginal debugging value,
could leak content size information.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix error message: evaluate_responses() → evaluate_traces(response_ids=...)

The referenced function doesn't exist; the correct API is
evaluate_traces(response_ids=...) from the azure-ai package.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove dead to_eval_data() method, fix docstring claims

- Remove to_eval_data() from EvalItem (dead code after r4-05 JSONL refactor)
- Migrate 15 tests from to_eval_data() to split_messages()
- Update sample to use split_messages() + Message properties
- Remove unimplemented Responses API optimization docstring claim
- Update split_messages() docstring to not reference removed method

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Reduce default eval timeout from 600s to 180s (3 minutes)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove dead _evaluate_via_responses method from FoundryEvals

The method was never called — evaluate() uses _evaluate_via_dataset,
and evaluate_traces() calls _evaluate_via_responses_impl directly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert unrelated formatting changes to get-started samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix pyright: remove phantom FoundryMemoryProvider import, apply ruff format

- Remove import of non-existent _foundry_memory_provider module
  (incorrectly kept during rebase conflict resolution)
- Apply ruff formatter to test_local_eval.py and get-started samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix eval samples: use FoundryChatClient for Agent()

The upstream provider-leading client refactor (#4818) made client=
a required parameter on Agent(). Update the three getting-started
eval samples to use FoundryChatClient with FOUNDRY_PROJECT_ENDPOINT,
matching the standard pattern from 01-get-started samples.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Simplify self-reflection sample using FoundryEvals

Replace ~80 lines of manual OpenAI evals API code (create_eval,
run_eval, manual polling, raw JSONL params) with FoundryEvals:

- evaluate_groundedness() uses FoundryEvals.evaluate() with EvalItem
- Remove create_openai_client(), create_eval(), run_eval() functions
- Remove openai SDK type imports (DataSourceConfigCustom, etc.)
- run_self_reflection_batch creates FoundryEvals instance once,
  reuses it for all iterations across all prompts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update eval samples to FoundryChatClient and FOUNDRY_PROJECT_ENDPOINT

- Migrate all foundry_evals samples from AzureOpenAIResponsesClient to FoundryChatClient
- Update env var from AZURE_AI_PROJECT_ENDPOINT to FOUNDRY_PROJECT_ENDPOINT
- Use AzureCliCredential consistently across all samples
- Fix README.md: correct function names (evaluate_dataset -> FoundryEvals.evaluate, evaluate_responses -> evaluate_traces)
- Update self_reflection .env.example and README.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix lint errors in eval samples (E501, ASYNC240, formatting)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove evaluate_all_patterns_sample.py (redundant with focused samples)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix async credential mismatch: use azure.identity.aio for async AIProjectClient

AIProjectClient from azure.ai.projects.aio requires an async credential.
Switch all foundry_evals samples from azure.identity.AzureCliCredential
to azure.identity.aio.AzureCliCredential. Also pass project_client to
FoundryChatClient instead of duplicating endpoint+credential.

Close credential in self_reflection sample to avoid resource leak.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert test_observability.py to upstream/main (not our test)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address moonbox3 review: sphinx docstrings, pagination, isinstance check

- Convert all Example:: / Typical usage:: code blocks to .. code-block:: python
  format matching codebase convention (both _evaluation.py and _foundry_evals.py)
- Add async pagination in _fetch_output_items via async for (handles large result sets)
- Replace hasattr(__aenter__) with isinstance(client, AsyncOpenAI) in _resolve_openai_client
- Move AsyncOpenAI import from TYPE_CHECKING to runtime (needed for isinstance)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix test failures and address remaining moonbox3 review comments

- Fix tests: use MagicMock(spec=AsyncOpenAI) for project_client mocks
  (isinstance check now requires proper type, not duck-typing)
- Fix tests: replace mock_page.__iter__ with _AsyncPage helper for async for
- Fix evaluate_response: auto-extract queries from response messages when
  query is not provided (previously always raised ValueError)
- Add debug logging when skipping internal _-prefixed executor IDs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Tao's PR review comments on Foundry Evals

- T1: Add comment explaining builtin.* pass-through in _resolve_evaluator
- T2: Add comment referencing OpenAI evals API for testing_criteria dict
- T3: Document Mustache-style {{item.*}} template placeholders
- T4: Document poll loop 60s sleep upper bound rationale
- T5: Narrow run type to RunRetrieveResponse, use typed field access
  instead of vars()/getattr dance in _extract_result_counts and
  _extract_per_evaluator; use run.error and run.report_url directly
- T6: Clarify openai_client docstring re: Azure Foundry endpoint
- T8: Remove misleading empty expected_tool_calls from sample
- Update tests to match real SDK PerTestingCriteriaResult shape

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove unnecessary Any union from run type annotations

RunRetrieveResponse is the correct type — no backward compat needed
for a brand new feature.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Accept FoundryChatClient instead of raw AsyncOpenAI

FoundryEvals now takes client: FoundryChatClient as its primary
parameter instead of openai_client: AsyncOpenAI.  The builtin.*
evaluators require a Foundry endpoint, so the type should reflect that.

- FoundryEvals.__init__: client: FoundryChatClient replaces openai_client
- evaluate_traces / evaluate_foundry_target: same change
- _resolve_openai_client: extracts .client from FoundryChatClient
- project_client fallback retained for standalone functions
- All samples updated to construct FoundryChatClient and pass as client=
- Tests updated (openai_client= → client=)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove implicit 60s upper bound on poll interval

If a developer sets a higher poll_interval, respect it. Only clamp
to remaining time and enforce a 1s minimum for rate-limit protection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove 1s floor on poll interval — let the developer control it

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update python/samples/05-end-to-end/evaluation/foundry_evals/.env.example

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Update python/samples/02-agents/evaluation/evaluate_agent.py

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Address eavanvalkenburg review (round 2) on Python eval PR

- Rename model_deployment -> model across FoundryEvals and all samples
- Make model param optional, resolves from client.model
- Convert EvalResults from dataclass to regular class
- Remove deprecated evaluate_response() function
- Refactor splitters: BUILT_IN_SPLITTERS dict + standalone functions
- Change per_turn_items from classmethod to staticmethod
- Simplify EvalCheck type alias to use Awaitable[CheckResult]
- Remove errored property from EvalResults
- Remove default value from Evaluator protocol eval_name
- Rename assert_passed -> raise_for_status, add EvalNotPassedError
- Type agent param as SupportsAgentRun | None
- Fix Arguments docstring
- Update __init__.py exports
- Update all tests and samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Move FoundryEvals to foundry package, split tool eval sample

- Move _foundry_evals.py from azure-ai to foundry package
- Move test_foundry_evals.py to foundry/tests/
- Update lazy re-exports in agent_framework.foundry namespace
- Update .pyi type stubs
- All samples now import from agent_framework.foundry
- Split tool-call evaluation into evaluate_tool_calls_sample.py
- Fix all_passed to check errored count from result_counts
- Fix raise_for_status to include errored item details

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Auto-create FoundryChatClient from env vars when no client provided

FoundryEvals() now works zero-config when FOUNDRY_PROJECT_ENDPOINT and
FOUNDRY_MODEL environment variables are set. Auto-creates a FoundryChatClient
under the hood, matching the established env var pattern.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix pyright errors: remove dead _normalize_queries, suppress EvalAPIError check

- Remove unused _normalize_queries function and its tests
- Add pyright ignore for EvalAPIError None check (defensive guard)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Support multimodal image content in eval pipeline

Add image (data/uri) content handling to AgentEvalConverter.convert_message()
so that Content.from_data() and Content.from_uri() image payloads are
preserved as input_image parts in the Foundry evaluator format.

- Handle Content type='data' and type='uri' → emit input_image parts
- Add 6 unit tests for image content through convert_message/convert_messages
- Add integration test verifying images flow through EvalItem → JSONL path
- Add evaluate_multimodal.py sample demonstrating local image eval

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address remaining review comments

- Fix project_client docstring to say async-only (not sync/async)
- Add builtin evaluator name validation warning in _resolve_evaluator
- Replace getattr with typed attribute access in _poll_eval_run,
  _extract_result_counts, _extract_per_evaluator, _fetch_output_items
- Remove cast import from _foundry_evals (no longer needed)
- Tighten _coerce_result: honour explicit 'passed' when both 'score'
  and 'passed' are present; remove performative cast
- Fix self_reflection sample: add env file existence check
- Fix traces sample: correct Pattern 2 section label
- Update all Foundry eval samples to FoundryChatClient + FOUNDRY_MODEL
  (remove AIProjectClient + AZURE_AI_MODEL_DEPLOYMENT_NAME pattern)
- Add eval_name and OpenAI client docs to FoundryEvals docstring
- Update test mocks to match typed SDK objects (_MockResultCounts)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix ruff lint errors (E501, SIM108, SIM102)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix pyright errors: type-narrow dict to dict[str, Any], add ignore comments

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Replace ConversationSplitter type alias with Protocol

ConversationSplitter is now a runtime-checkable Protocol with a named
'conversation' parameter, making the expected signature self-documenting.

ConversationSplit enum members gain a __call__ method so they satisfy
the protocol directly -- ConversationSplit.LAST_TURN(conversation) works.

This simplifies _split_conversation from an isinstance dispatch to a
single split(conversation) call.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Standardize on AZURE_AI_MODEL_DEPLOYMENT_NAME and fix Unicode in samples

- Replace FOUNDRY_MODEL with AZURE_AI_MODEL_DEPLOYMENT_NAME in all
  eval samples to match repo convention
- Replace Unicode symbols with ASCII equivalents in all eval sample
  print statements to avoid cp1252 encoding errors on Windows

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update python/samples/03-workflows/evaluation/evaluate_workflow.py

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Rename ADR 0020 to 0023 (foundry evals integration)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-03-31 15:53:06 +00:00
3f964c4cdb Removing old code-gen docs from dotnet root (#4997)
Co-authored-by: alliscode <bentho@microsoft.com>
2026-03-31 15:37:59 +00:00
Tao ChenandGitHub 016daf3b98 Python: Fix samples (#4980)
* First samples 1st batch

* Fix sample paths

* Fix workflow samples

* Fix workflow dependency

* Correct env vars

* Increase idle timeout

* Fix workflows HIL sample

* Fix more workflow samples
2026-03-31 15:20:35 +00:00
0f81c277d9 Updated package versions (#4982)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-30 21:26:05 +00:00
Giles OdigweandGitHub 0e00e5f8dd Python: Update Python Packages for rc6 (#4979)
* python package update

* small fix
2026-03-30 21:12:37 +00:00
westeyandGitHub 31c866172a Fix broken url in samples (#4981) 2026-03-30 19:19:16 +00:00
401e5dc7e8 .NET: Allow Simulating service stored ChatHistory to improve consistency (#4974)
* Allow Simulating service stored ChatHistory to improve consistency

* Fixing bug in ServiceStoredSimulatingChatClient

* Addressing PR comments.

* Address PR comments

* Apply suggestion from @SergeyMenshykh

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>

* Fix bug

---------

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
2026-03-30 18:52:01 +00:00
18f7ba8632 .NET: Add API breaking change validation for RC packages (#4977)
* Add API breaking change validation for RC packages

Enable .NET Package Validation for release candidate packages to detect
API breaking changes in CI. This follows the same pattern used by
Semantic Kernel, centralized through nuget-package.props.

Changes:
- Enable EnablePackageValidation for IsReleaseCandidate packages
- Update PackageValidationBaselineVersion to 1.0.0-rc4 (latest published)
- Generate CompatibilitySuppressions.xml for existing known API changes
  in 5 packages (AI, AzureAI, OpenAI, Workflows, Workflows.Declarative.AzureAI)
- Opt out Workflows.Declarative.Mcp (not yet published to NuGet)
- Add breaking changes guidance to CONTRIBUTING.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review feedback

- Remove unnecessary empty PackageValidationBaselineVersion override
  in Workflows.Declarative.Mcp.csproj (EnablePackageValidation=false
  is sufficient)
- Tighten CONTRIBUTING.md wording to clarify opt-out possibility

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Enable package validation for GA packages (no VersionSuffix)

Expand the EnablePackageValidation condition to also cover future GA
packages that have no VersionSuffix, not just RC packages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix EnablePackageValidation GA condition to check PackageVersion

The previous condition VersionSuffix=='' matched all packages (preview
included) since VersionSuffix defaults to empty. Now uses two separate
conditions: one for RC, one for true GA (PackageVersion == VersionPrefix).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add IsGeneralAvailable flag for package validation

Replace fragile PackageVersion condition with explicit IsGeneralAvailable
property, following the same per-project self-declaration pattern as
IsReleaseCandidate.

- Directory.Build.props: Add IsGeneralAvailable=false default
- nuget-package.props: EnablePackageValidation on RC OR GA
- CONTRIBUTING.md: Update docs to mention both flags

When packages go GA, they set IsGeneralAvailable=true in their .csproj.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Rename IsGeneralAvailable to IsGenerallyAvailable

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-30 17:36:43 +00:00
05c53dce2d Suppress CodeQL false positive (#4948)
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2026-03-30 16:34:45 +00:00
ade295b122 .NET: Add inline skills API (#4951)
* add inline skills

* Fix IDE1006 and IDE0004 formatting errors in test files

- Add 'Async' suffix to async test methods in FilteringAgentSkillsSourceTests,
  DeduplicatingAgentSkillsSourceTests, and AgentInMemorySkillsSourceTests
- Use pragma to suppress false-positive IDE0004 on casts needed for overload
  disambiguation in AgentInlineSkillTests and AgentInlineSkillResourceTests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* address issues

* address comments

* make inline skills script and resource model classes internal

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-30 16:23:04 +00:00
CopilotGitHubrogerbarretocopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
4527dee64b Python: fix: update PyRIT repository link from Azure/PyRIT to microsoft/PyRIT (#4960)
* Initial plan

* fix: update PyRIT repository link from Azure/PyRIT to microsoft/PyRIT

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/830b8ccf-a79c-49b6-90c9-3bb3e740bc06

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
2026-03-30 14:48:08 +00:00
f45fc7d402 .NET: [Breaking] Update Foundry Agents for Responses API (#4502)
* Stage

* Add FoundryAgentClient, model param, chatClientFactory, and RAPI samples

- Add model parameter to FoundryAgentClient simple constructor
- Add chatClientFactory parameter to both constructors
- Switch to OpenAI.GetProjectResponsesClientForModel for direct Responses API usage
- Add FoundryAgents-RAPI samples (Step01 Basics, Step02 Multiturn, Step03 FunctionTools)
- Add solution folder entry for FoundryAgents-RAPI samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add auto-discovery constructor and simplify RAPI samples

- Add FoundryAgentClient constructor that reads AZURE_AI_PROJECT_ENDPOINT and
  AZURE_AI_MODEL_DEPLOYMENT_NAME from environment variables with DefaultAzureCredential
- Simplify RAPI samples to use auto-discovery (no env var or credential code)
- Remove Azure.Identity direct references from sample csproj files
- Update READMEs to document environment variable requirements

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add remaining RAPI samples (Step04-Step12)

- Step04: Function tools with human-in-the-loop approvals
- Step05: Structured output with typed responses
- Step06: Persisted conversations with session serialization
- Step07: Observability with OpenTelemetry
- Step08: Dependency injection with hosted service
- Step10: Image multi-modality
- Step11: Agent as function tool (agent composition)
- Step12: Middleware (PII, guardrails, function logging, HITL approval)
- Update solution file and folder README with all new samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add all RAPI samples (Step09-Step23) and switch to AzureCliCredential

- Step09: MCP client as tools (GitHub server via stdio)
- Step13: Plugins with dependency injection
- Step14: Code Interpreter tool
- Step15: Computer Use tool with screenshot simulation
- Step16: File Search with vector stores
- Step17: OpenAPI tools (REST Countries API)
- Step18: Bing Custom Search
- Step19: SharePoint grounding
- Step20: Microsoft Fabric
- Step21: Web Search with citations
- Step22: Memory Search with multi-turn conversations
- Step23: Local MCP via HTTP (Microsoft Learn)
- Switch all samples (Step04-Step12) to use AzureCliCredential with env vars
- Update solution file and README with all 23 samples
- All 23 samples build successfully, tested Step05/06/11/13/21

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Switch Step01-03 samples to AzureCliCredential for consistency

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clarify connection ID format in SharePoint and Fabric READMEs

Document that SHAREPOINT_PROJECT_CONNECTION_ID and FABRIC_PROJECT_CONNECTION_ID
should use the connection name (e.g., 'SharepointTestTool'), not the full ARM
resource URI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Normalize env vars, fix structured output, update READMEs with connection ID formats

- Normalize AZURE_FOUNDRY_PROJECT_* env vars to AZURE_AI_PROJECT_ENDPOINT / AZURE_AI_MODEL_DEPLOYMENT_NAME across all samples (Steps 18-22 READMEs + Steps 19-20 Program.cs)
- Fix RAPI Step05 StructuredOutput to use full constructor with ResponseFormat for streaming JSON
- Update Deep Research sample to use AzureCliCredential
- Enrich Bing Grounding README with full ARM resource URI format
- Fix Bing Custom Search README env var mismatch (BING_CUSTOM_SEARCH_* -> AZURE_AI_CUSTOM_SEARCH_*)
- Add finding instructions for connection ID and instance name in Bing Custom Search READMEs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Refactor memory samples and switch to DefaultAzureCredential

- Refactor RAPI Step22 MemorySearch: extract store setup to EnsureMemoryStoreAsync local function
- Refactor non-RAPI Step22 MemorySearch: same pattern with explicit memory lifecycle
- Set UpdateDelay=0 on MemoryUpdateOptions and MemorySearchPreviewTool for faster ingestion
- Use WaitForMemoriesUpdateAsync with 500ms polling interval
- Switch Step19 SharePoint, Step20 Fabric, Step22 MemorySearch (both) to DefaultAzureCredential
- Remove SearchOptions from MemorySearchPreviewTool (causes unknown parameter error)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Switch all RAPI samples to DefaultAzureCredential and format

- Replace AzureCliCredential with DefaultAzureCredential across all 20 RAPI samples
- Run dotnet format on all RAPI and non-RAPI Foundry samples
- AzureAI unit tests: 341 passed (net10.0 + net472)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Rename to Microsoft Foundry, add metadata, rename RAPI folder

- Replace 'Azure AI Foundry' / 'Azure Foundry' with 'Microsoft Foundry' in all docs, comments, and XML docs
- Update FoundryAgentClient metadata provider name to 'microsoft.foundry'
- Rename FoundryAgents-RAPI folder to FoundryResponseAgents
- Rewrite FoundryResponseAgents README with comparison table vs Foundry Agents
- Update slnx and parent README with new folder references

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review: simplify sample comments and fix DeepResearch credential

- Remove 'no server-side agent' and 'Responses API directly' phrasing from comments
- Simplify to 'Create a FoundryAgentClient' per review feedback
- Switch Agent_Step15_DeepResearch to DefaultAzureCredential

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Restore full DefaultAzureCredential warning comment in DeepResearch sample

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add ADR 0020: Foundry agent type naming convention

Proposes naming options for a new MAF type wrapping versioned
Foundry agents (Prompt, ContainerApp, Hosted, Workflow) to
distinguish from the existing FoundryResponsesAgent (RAPI path).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Simplify FoundryResponsesAgent samples with env-var constructors and rename folders

- Add env-var constructors to FoundryResponsesAgent (simple + options-based)
- Fix Constructor 1 model optionality (no longer throws on missing AZURE_AI_MODEL_DEPLOYMENT_NAME)
- Add ApplyModelDeploymentFallback helper for options-based constructor
- Update all 23 FoundryResponseAgents samples to remove Environment.GetEnvironmentVariable boilerplate
- Condense 6 simple samples to one-liner constructor calls
- Add XML doc remarks about auto-resolved parameters on all constructors
- Rename FoundryAgents -> FoundryVersionedAgents (server-side, versioned)
- Rename FoundryResponseAgents -> FoundryAgents (now the default path forward)
- Update .slnx and README cross-references for new folder names

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add FoundryAITool factory, rename RAPI folders, and clean up references

- Create FoundryAITool static factory class with 17 methods wrapping AgentTool.Create* and ResponseTool.Create* into AITool returns
- Rename 23 FoundryAgentsRAPI_* subfolders to FoundryAgents_* (drop RAPI prefix)
- Rename .csproj files and update .slnx references accordingly
- Update 12 samples (6 FoundryAgents + 6 FoundryVersionedAgents) to use FoundryAITool
- Replace all FoundryResponsesAgent references with FoundryAgent in comments and READMEs
- Update sample READMEs to reference FoundryAITool methods

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Rename FoundryVersionedAgents subfolders from FoundryAgents_* to FoundryVersionedAgents_*

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add FoundryVersionedAgent class and refactor extension method internals

- Create FoundryVersionedAgent with private ctor and async static factory methods
  (CreateAIAgentAsync/GetAIAgentAsync) with env-var and explicit endpoint tiers
- Extract shared internal helpers from AzureAIProjectChatClientExtensions:
  CreateChatClientAgent, CreateAgentVersionFromOptionsAsync,
  CreateAgentVersionWithProtocolAsync (tools overload),
  CreateChatClientAgentOptions, GetAgentRecordByNameAsync, ThrowIfInvalidAgentName
- Extension methods now delegate to shared internal helpers
- All 49 existing samples continue to build successfully

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add CreateConversationSessionAsync, DeleteAIAgentAsync, auto-resolve model, simplify samples

- Add CreateConversationSessionAsync to FoundryAgent and FoundryVersionedAgent
  (returns ChatClientAgentSession, creates server-side conversation + session in one call)
- Add DeleteAIAgentAsync static method to FoundryVersionedAgent
- Make model parameter optional in env-var factory overloads (auto-resolves from
  AZURE_AI_MODEL_DEPLOYMENT_NAME)
- Update all FoundryVersionedAgents samples to use DeleteAIAgentAsync
- Remove deploymentName env var from samples where only used for model parameter
- Use CreateConversationSessionAsync in Step02_MultiturnConversation
- Use explicit types instead of var for agent/session variables
- All 49 samples build successfully

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove manual AIProjectClient construction from FoundryVersionedAgents samples

- Replace manual AIProjectClient construction with GetService<AIProjectClient>()
  from the FoundryVersionedAgent in all dual-option and tool-specific samples
- Remove AZURE_AI_PROJECT_ENDPOINT env var reads from updated samples
- Remove Azure.Identity usings where no longer needed
- Only Step01.1, Step01.2, Eval_Step01 retain manual construction (pedagogical samples)
- All 49 samples build successfully

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Replace aiProjectClient extension calls with FoundryVersionedAgent factories in all samples

- Replace aiProjectClient.CreateAIAgentAsync with FoundryVersionedAgent.CreateAIAgentAsync
  in Option 2 (Native SDK) paths across Steps 14-21
- Replace aiProjectClient.Agents.DeleteAgentAsync with FoundryVersionedAgent.DeleteAIAgentAsync
- Remove unused AIProjectClient variables and using directives
- Only Step01.1, Step01.2, Eval_Step01 retain direct AIProjectClient usage (pedagogical)
- Step16, Step22 use GetService<AIProjectClient>() for file/memory operations

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove unused using directives from Step01.2, Step09, Eval_Step02

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update ADR 0020 with accepted decision: Option 6

- Add Option 6 detailing FoundryAgent, FoundryVersionedAgent, FoundryAITool,
  env-var auto-discovery, and self-contained factory patterns
- Mark decision as accepted with rationale
- Update current state and metadata sections

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update Step01 basics samples to use FoundryVersionedAgent factories

- Step01.1: Replace manual AIProjectClient/AsAIAgent with FoundryVersionedAgent.CreateAIAgentAsync/GetAIAgentAsync/DeleteAIAgentAsync
- Step01.2: Replace manual AIProjectClient with FoundryVersionedAgent.CreateAIAgentAsync/DeleteAIAgentAsync
- Remove env var boilerplate and Azure.Identity dependency

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add DeleteAIAgentVersionAsync to FoundryVersionedAgent

- DeleteAIAgentAsync: deletes the agent and all its versions (existing)
- DeleteAIAgentVersionAsync: deletes only the specific version associated with the agent instance
- Internally delegates to Agents.DeleteAgentAsync vs Agents.DeleteAgentVersionAsync

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix cleanup comments: DeleteAIAgentAsync deletes the agent and all its versions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update all FoundryVersionedAgents READMEs for FoundryVersionedAgent and auto-discovery

- Rewrite main README with FoundryVersionedAgent usage, auto-discovery table, code example
- Fix sample table links from FoundryAgents_Step* to FoundryVersionedAgents_Step*
- Add FoundryAITool references in tool-specific sample descriptions
- Update individual READMEs: fix stale paths, add auto-discovery note after env var blocks
- Update tool references: AgentTool/ResponseTool -> FoundryAITool
- Update parent 02-agents/README.md with FoundryVersionedAgent description

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert unrelated AGUI and Hosting.OpenAI formatting changes to main

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove env-var auto-discovery, add AsAIAgent, mark extensions Obsolete

- Remove 2 env-var constructors from FoundryAgent (keep explicit endpoint ctors)
- Remove 5 env-var factory methods from FoundryVersionedAgent (keep explicit ones)
- Add 3 AsAIAgent static methods to FoundryVersionedAgent (AgentVersion/AgentRecord/AgentReference)
- Mark all 8 AIProjectClient extension methods as [Obsolete] pointing to FoundryVersionedAgent
- Remove ApplyModelDeploymentFallback, env var constants, Azure.Identity usings from source

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update all samples to use explicit endpoint, credential, and model parameters

- Add explicit Environment.GetEnvironmentVariable reads for AZURE_AI_PROJECT_ENDPOINT
  and AZURE_AI_MODEL_DEPLOYMENT_NAME to all 48 sample files
- Pass new Uri(endpoint), new DefaultAzureCredential(), deploymentName to
  FoundryAgent constructors and FoundryVersionedAgent factory methods
- Add using Azure.Identity where missing
- Matches repo-wide pattern used by other non-Foundry samples
- All 49 samples build successfully

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Migrate remaining samples and source from obsoleted extension methods

- Migrate AgentProviders, AgentWithRAG, AgentWithMemory, HostedWorkflow samples to FoundryVersionedAgent
- Migrate AzureAgentProvider.cs to FoundryVersionedAgent.AsAIAgent
- Migrate AzureAIProjectChatClientTests.cs to FoundryVersionedAgent.GetAIAgentAsync
- Remove pragma suppressions from migrated files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add unit tests for FoundryAgent and FoundryVersionedAgent

- FoundryAgentTests.cs: 14 tests covering constructors, validation,
  properties, metadata, GetService, chat client factory, user-agent header
- FoundryVersionedAgentTests.cs: 31 tests covering CreateAIAgentAsync,
  GetAIAgentAsync, AsAIAgent (3 overloads), DeleteAIAgentAsync,
  DeleteAIAgentVersionAsync, validation, invalid names, metadata, GetService

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Finalize Foundry agent migration

Align FoundryAgent and FoundryVersionedAgent samples, docs, and tests with the explicit configuration model, clean up stale README guidance, and fix AzureAI unit test validation/build issues.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Apply formatter cleanup after validation

Capture the dotnet format follow-up changes produced during branch validation so the committed state matches the successfully built and tested branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add integration tests for FoundryAgent and FoundryVersionedAgent

Mark old AIProjectClient extension-method integration tests as obsolete and add new integration test suites for both FoundryAgent (Responses API) and FoundryVersionedAgent (versioned agents). All 71 non-skipped tests pass against the live Foundry service.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update ADR 0020 with test coverage details

Add integration test coverage note to the Current State section of ADR 0020.

* Simplify Foundry agents and validate moved samples

* Rename FoundryAgent integration tests to ResponsesAgent

The test classes exercise the non-versioned Responses path via
AIProjectClient.AsAIAgent(), not the removed FoundryAgent wrapper type.
Rename files and class names to reflect the actual test surface.

* Update documentation for ChatClientAgent usage

Added example usage of ChatClientAgent with JokerAgent.

* Refactor ChatClientAgent instantiation for clarity

* Revise agent type naming and usage examples

Updated documentation to reflect changes in agent creation methods and added examples for using `ChatClientAgent`.

* Fix Azure SDK namespace migration after rebase

Update Azure.AI.Projects.OpenAI references to Azure.AI.Projects.Agents
and Azure.AI.Extensions.OpenAI to match Azure.AI.Projects 2.0.0-beta.2.

- Replace deprecated namespace across samples, tests, and src
- Fix renamed types: OpenAPIFunctionDefinition -> OpenApiFunctionDefinition,
  BingCustomSearchToolParameters -> BingCustomSearchToolOptions,
  BrowserAutomationToolParameters -> BrowserAutomationToolOptions
- Fix API changes: AgentRecord.Versions -> GetLatestVersion(),
  ResponsesClient constructor, FunctionApprovalRequestContent ->
  ToolApprovalRequestContent
- Apply dotnet format

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address merge markers

* Replace obsolete GetAIAgentAsync with AsAIAgent in samples

Switch Agent_Step07_AsMcpTool and A2AServer to use the non-obsolete
PersistentAgentsClient.AsAIAgent(PersistentAgent) extension instead
of the deprecated GetAIAgentAsync, fixing CS0618 build errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix broken markdown links in Responses sample READMEs

Replace stale ChatClientAgents_Step* folder references with the
correct Agent_Step* names across all Responses sample READMEs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix format errors and address PR review comments

- Fix charset and remove unused using in AzureAIProjectResponsesChatClient
- Fix doc comment tags (code -> c) in FoundryAITool
- Fix stray period in LocalMCP sample comment
- Fix grammar in FoundryMemoryProvider xmldoc
- Fix AIProjectClientAgentRunStreamingConversationTests base class

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Apply dotnet format fixes to PR-changed files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix build errors from format pass and apply naming conventions

- Fix static call to CreateSessionAsync in Step02 samples and extension tests
- Use expression-bodied lambda in FoundryMemoryProvider (RCS1021)
- Apply PascalCase naming to const fields in ResponsesAgentExtensionCreateTests (IDE1006)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Introduce FoundryAgent sealed type and update AsAIAgent extensions

- Add FoundryAgent sealed class wrapping ChatClientAgent with:
  - Public ctors: (projectEndpoint, credential, model, instructions) and (agentEndpoint, credential)
  - Internal ctor: (AIProjectClient, ChatClientAgent) for extension use
  - CreateConversationSessionAsync() for server-side conversations
  - GetService<ChatClientAgent>() and GetService<AIProjectClient>()
  - MEAI user-agent policy on internally-created AIProjectClient
- Change all AsAIAgent extension return types from ChatClientAgent to FoundryAgent
- Update all samples and tests to use FoundryAgent type
- Add 16 FoundryAgentTests covering ctors, GetService, UserAgent, RunAsync
- Fix pre-existing Agent_Step12_Plugins build error

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Collapse sample folders and add FoundryAgent_Step01 sample

- Move all Responses/* samples up to AgentsWithFoundry/ (flat structure)
- Remove entire Versioned/ folder (26 samples)
- Add FoundryAgent_Step01 sample showing direct FoundryAgent ctor usage
- Update slnx to reflect flat folder structure
- Fix csproj ProjectReference paths for new depth

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update READMEs for flat AgentsWithFoundry structure

- Rewrite AgentsWithFoundry/README.md with FoundryAgent quick start
- Fix cd commands and paths in 11 sample READMEs
- Update 02-agents/README.md to single Foundry link
- Update AGENTS.md tree to flat structure
- Fix AgentWithMemory cross-reference

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix FoundryAgent_Step01 sample with full create/run/delete lifecycle

Show the complete server-side agent lifecycle: create version with
native SDK, wrap as FoundryAgent via AsAIAgent, run, then delete.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert RAPI samples to use AIAgent instead of FoundryAgent

RAPI samples should not reference FoundryAgent directly. Restored
original sample code with only ChatClientAgent -> AIAgent type change
to accommodate the AsAIAgent return type.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Convert versioned-pattern samples to pure RAPI

Step09, Step13, Step17, Step22 were using CreateAgentVersionAsync +
PromptAgentDefinition which is the versioned pattern. Converted to
use AsAIAgent(model, instructions, tools) which is the RAPI path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix format issues from Docker CI check

- FoundryAgent_Step01: CRLF -> LF
- Agent_Step09: missing final newline
- Agent_Step11_Middleware: add internal modifier, final newline
- Agent_Step02: remove redundant cast (IDE0004)
- Agent_Step08: simplify name (IDE0001)
- FoundryAgentTests: s_ prefix, Async suffix naming conventions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Switch Step09 MCP sample to Microsoft Learn HTTP endpoint

Replace npx stdio GitHub MCP server with the public Microsoft Learn
MCP endpoint (https://learn.microsoft.com/api/mcp) using HTTP transport.
No external tooling required to run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix missing final newline in Step09 MCP sample

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review: use DelegatingAIAgent, clean up Step01 sample

- FoundryAgent now inherits DelegatingAIAgent instead of AIAgent,
  removing manual delegation boilerplate (westey-m feedback)
- Simplified Agent_Step01_Basics to single agent creation path,
  moved composable IChatClient approach to README (westey-m feedback)
- Fixed FoundryAgentTests param name assertion

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update sample using Project specialized type instead

* Address PR review feedback: DefaultAzureCredential warnings, sample simplifications, format fixes

- Add DefaultAzureCredential production warning comments to ~25 samples
- Simplify Anthropic and OpenAI Step01 samples to single agent
- Convert Step11 Middleware regex patterns to [GeneratedRegex]
- Remove unnecessary cleanup comment from Step06
- Fix Step09 README MCP transport description
- Enhance FoundryAgent xmldoc with non-persistent agent comparison

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Split Step02, simplify RAG Step04, sharpen Step23 differentiation

- Split Step02 into 02.1 (simple multi-turn via sessions) and 02.2 (server-side conversations via CreateConversationSessionAsync)
- RAG Step04: replace HostedFileSearchTool + MEAI wrapping with native OpenAI FileSearchTool
- Step23: clarify DelegatingAIFunction wrapping pattern vs Step09 basic MCP

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Hosted MCP sample: use ResponseTool.CreateMcpTool and move tool to PromptAgentDefinition

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix broken README link after Step02 split

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address Sergey round 3 feedback: branding, README nav, sample rename

- Replace 'Azure AI Foundry' with 'Microsoft Foundry' in ADR 0020
- Fix 3 READMEs: 'ChatClientAgents' → 'AgentsWithFoundry' sample directory
- Rename FoundryAgent_Step01 → Agent_Step00_FoundryAgentLifecycle for naming consistency

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-30 12:09:02 +00:00
ca02146ee4 .NET: BugFix #3433 ChatClientAgent streaming responses missing messageid (#4615)
* Changes

* Fix ChatClientAgent streaming responses missing MessageId

Generate fallback MessageId in ChatClientAgent.RunCoreStreamingAsync when
the underlying LLM provider does not set ChatResponseUpdate.MessageId.
Without a MessageId the AGUI converter's null==null check silently drops
all text content, causing CopilotKit Zod validation errors.

Changes:
- ChatClientAgent: generate msg_{Guid} fallback via ??= in streaming loop
- AgentResponseExtensions: sync wrapper MessageId back to RawRepresentation
  in AsChatResponseUpdate() so downstream consumers see the value
- Add unit tests for both fixes and AGUI streaming MessageId scenarios

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR #4615 review comments

- Fix MessageId seeding: use first-seen provider MessageId (or generate
  fallback) and apply consistently to all chunks in the stream, preventing
  message splitting when providers set MessageId only on the first chunk
- Add test for mixed MessageId scenario (first chunk only)
- Fix skipped TextStreaming test: assert Empty (not NotEmpty) to match
  actual null==null behavior
- Fix skipped ToolCalls test: assert empty ParentMessageId to match
  actual empty-string passthrough behavior

* Handle empty MessageId in AsChatResponseUpdate sync

Treat empty/whitespace MessageId the same as null when syncing from
the AgentResponseUpdate wrapper back to RawRepresentation. Providers
that return empty string MessageId (e.g. tool call responses) now get
the wrapper value recovered correctly.

Add test for empty string MessageId recovery scenario.

* Move MessageId fallback generation to AGUI layer

Move fallback MessageId generation from ChatClientAgent to
AsAGUIEventStreamAsync, addressing the architectural concern that
MessageId is nullable in the AIAgent abstraction and the requirement
for non-null values is specific to the AGUI protocol.

The AGUI layer now generates a fallback MessageId for null or
empty/whitespace values, covering all agent types (not just
ChatClientAgent) including external implementations.

Changes:
- Revert MessageId generation from ChatClientAgent.RunCoreStreamingAsync
- Add fallback MessageId generation in AsAGUIEventStreamAsync for
  null/empty MessageId values (handles both null and whitespace)
- Unskip and update AGUI tests to verify fallback generation
- Update ChatClientAgent tests to reflect passthrough behavior

* Revert AsChatResponseUpdate MessageId sync-back

Remove the MessageId sync-back logic from AsChatResponseUpdate() as it
is no longer needed. With fallback generation moved to the AGUI layer,
the abstraction layer should not mutate the RawRepresentation object.

Revert to the original passthrough behavior for AsChatResponseUpdate()
and update tests accordingly.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-30 11:55:30 +00:00
3627b9b911 .NET: Skip flaky integration tests blocking merge queue (#4972)
* Skip flaky integration tests blocking merge queue

Skip 7 timing-dependent integration tests that are causing a 73% failure
rate in the merge queue over the past 3 days. All failures are
LLM-latency/timeout related and occur across unrelated PRs.

Skipped tests:
- ConsoleAppSamplesValidation.ReliableStreamingSampleValidationAsync
- ConsoleAppSamplesValidation.SingleAgentOrchestrationHITLSampleValidationAsync
- ConsoleAppSamplesValidation.LongRunningToolsSampleValidationAsync
- SamplesValidation.LongRunningToolsSampleValidationAsync (AzureFunctions)
- SamplesValidation.ReliableStreamingSampleValidationAsync (AzureFunctions)
- ExternalClientTests.CallFunctionToolsAsync
- ExternalClientTests.CallLongRunningFunctionToolsAsync

Tracking issue: https://github.com/microsoft/agent-framework/issues/4971

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Extract skip reason to const per review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-30 10:51:21 +00:00
b1b528e4a8 Python: [BREAKING] Remove deprecated kwargs compatibility paths (#4858)
* [BREAKING] Remove deprecated kwargs compatibility paths

Remove the deprecated kwargs compatibility shims across core agents, clients, tools, middleware, and telemetry.

Keep workflow kwargs behavior intact in this branch and follow up separately in #4850.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix PR CI fallout for kwargs removal

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* updates

* Fix Azure AI CI fallout

Remove the stale _get_current_conversation_id override from the Azure AI client after the OpenAI base helper was deleted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fixed new classes

* Fix Assistants deprecated import gating

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix integration replay regressions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Switch multi-agent hosting samples to Azure chat completions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Simplify Azure multi-agent sample config

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-27 21:00:12 +00:00
westeyandGitHub ca6cdd142e .NET: Fixes for durable agents integration tests (#4952)
* Fixing for durable agents integration tests

* Add further fixes
2026-03-27 17:38:23 +00:00
6b47cdbf52 Python: Fix broken samples for GitHub Copilot, declarative, and Responses API (#4915)
* Python: Fix broken samples for GitHub Copilot, declarative, and Responses API

- Add missing on_permission_request handler to github_copilot_basic and
  github_copilot_with_session samples (required by copilot SDK)
- Increase timeout for remote MCP query in github_copilot_with_mcp sample
- Soften session isolation claim in github_copilot_with_session sample
- Fix inline_yaml sample: pass project_endpoint via client_kwargs instead
  of relying on YAML connection block (AzureAIClient expects
  project_endpoint, not endpoint)
- Handle raw JSON schemas in Responses client _convert_response_format
  so declarative outputSchema works with the Responses API

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Improve raw JSON schema detection heuristic and add tests

- Broaden raw schema detection to handle anyOf, oneOf, allOf, $ref, $defs
  keywords and JSON Schema primitive types, not just 'properties'
- Apply same raw schema handling to azure-ai _shared.py for consistency
- Add unit tests for both openai and azure-ai response_format conversion

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-27 16:27:19 +00:00
cc0cfaaac8 [BREAKING] Python: fix OpenAI Azure routing and provider samples (#4925)
* Python: fix OpenAI Azure routing and provider samples

Prefer OpenAI when OPENAI_API_KEY is present unless Azure is explicitly requested. Clarify constructor docs, keep deprecated Azure wrappers compatible with stricter settings validation, and refresh the provider samples and tests to use the current client patterns.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix bandit

* Python: align OpenAI embedding Azure routing

Extend the shared OpenAI-vs-Azure routing and credential behavior to the embedding client, add Azure embedding regression coverage, and refresh the embedding samples to use the generic client path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: fix embedding client pyright check

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: thin OpenAI embedding wrapper

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: document embedding overload routing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: fix callable OpenAI key routing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: fix Azure credential routing tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: address OpenAI review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: narrow Azure routing markers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: refine OpenAI model fallback order

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: narrow Azure deployment docs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: remove embedding routing wording

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: run embedding Azure integration tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* changed variable name

* Python: expand OpenAI package README

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* clarified readme

* Python: fix Azure OpenAI integration setup

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: correct Azure integration env mapping

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* updated code to fix int tests

* test updates

* test fix

* fix test setup

* updates to tests and setup

* remove openai assistants int tests

* improvements in int tests

* fix env var

* fix env vars

* fix azure responses test

* trigger actions

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-27 13:33:39 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3611be82cf Bump flatted from 3.3.3 to 3.4.2 in /python/packages/devui/frontend (#4805)
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 03:43:25 +00:00
SergeyMenshykhandGitHub 0fcbe7e105 .NET: [Breaking] Restructure agent skills to use multi-source architecture (#4871)
* initial commit

* address comments

* address comments

* address comments

* address  comments

* rename executor to runner to align naming with python implementation

* rename runner execute method to run method

* remove poc leftovers and fix compilation issues

* make script runner optional

* remove unnecessary pragmas

* make resources and scripts props virtual

* address comments

* update comment for name validation regex

* address comments
2026-03-26 22:27:17 +00:00
Jacob AlberandGitHub 5530bc536b .NET: feat: Implement return-to-previous routing in handoff workflow (#4356)
* feat: Implement return-to-previous routing in handoff workflow

- Also obsoletes HandoffsWorkflowBuilder => HandoffWorkflowBuilder (no "s")

* refactor: Remove instance-shared current agent tracking in handoffs

Because the tracker was instance-shared between the start and end executors, it would be shared between all sessions, resulting in incorrect behaviour.

The corect way to do this is to keep the data in a shared executor scope, which is per-session.

* fix: Fix test logic for Handoff to correctly use checkpointing for multiturn
2026-03-26 20:17:57 +00:00
9bfa593ae7 Python: Move ag_ui_workflow_handoff demo from demos/ to 05-end-to-end/ (#4900)
* Move ag_ui_workflow_handoff demo to 05-end-to-end (#4895)

Move the AG-UI workflow handoff demo from python/samples/demos/ to
python/samples/05-end-to-end/ to follow the current folder structure
convention. Update README paths accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix review feedback: remove build artifacts, fix README paths (#4895)

- Add .gitignore to frontend/ to exclude *.tsbuildinfo, vite.config.js,
  and vite.config.d.ts build artifacts from version control
- Remove the 4 tracked build artifact files from the tree
- Fix step 2 cd path in README to be relative after 'cd python'

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clarify working directory context in README Step 2 (#4895)

Step 2 uses a python/-relative path (samples/...) which assumes the
user is still in the python/ directory from Step 1. Add a brief note
making this explicit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 17:52:07 +00:00
3585581c7a .NET: Fix bug with per-service-call persistence and approvals (#4933)
* Fix bug with per-service-call persistence and approvals

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-26 17:45:46 +00:00
63dee91a5f .NET: Improve observability sample (#4917)
* Improve .Net observability sample

* Update dotnet/samples/02-agents/AgentOpenTelemetry/Program.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-26 17:33:22 +00:00
westeyandGitHub 3b8b56e6ea Samples fix (#4932) 2026-03-26 16:45:01 +00:00
9691c9c271 .NET: Fix role assignment in ChatMessage construction (#4290)
* Use actual message role when creating ChatMessage

Replace hard-coded ChatRole.User with a ChatRole constructed from the message's Role. The change ensures ToChatMessage and FunctionMessage use the original role (new ChatRole(this.Role)) for both text and contents branches, fixing incorrect role assignment when constructing ChatMessage instances.

* Update changes

* Fix formatting in ToChatMessage tests

---------

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2026-03-26 16:13:00 +00:00
d2977d63da .NET: Add integration test validating OpenAPI tools with AsAIAgent(agentVersion) (#4931)
* .NET: Add integration test for OpenAPI tools with AsAIAgent(agentVersion)

Validates end-to-end flow creating a Foundry agent with an OpenAPI tool
definition via native Azure.AI.Projects SDK types and wrapping it with
AsAIAgent(agentVersion). The test confirms the server-side OpenAPI
function is invoked correctly through RunAsync.

Addresses #4883

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review: RetryFact, PascalCase naming, stronger tool assertion

- Use RetryFact with Skip for manual testing (flaky due to external API)
- Fix agentName -> AgentName to match PascalCase convention in file
- Strengthen tool invocation assertion: require >= 3 Eurozone countries
- Add comment explaining server-side OpenAPI tools don't surface as
  FunctionCallContent in the MEAI abstraction

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 15:35:16 +00:00
84fe6c46ab .NET: Add AsIChatClientWithStoredOutputDisabled for ProjectResponsesClient (#4911)
* Add AsIChatClientWithStoredOutputDisabled for ProjectResponsesClient

Add extension method on ProjectResponsesClient in Microsoft.Agents.AI.AzureAI
package (Azure.AI.Extensions.OpenAI namespace) mirroring the existing extension
on ResponsesClient in the OpenAI package. This enables Azure AI consumers to
disable server-side response storage without depending on the OpenAI package.

- New ProjectResponsesClientExtensions class with AsIChatClientWithStoredOutputDisabled
- Optional deploymentName parameter (model is no longer required)
- Updated OpenAI counterpart doc to remove 'Required' wording for model param
- Added unit tests covering null guard, inner client accessibility,
  StoredOutputEnabled=false, and reasoning encrypted content inclusion/exclusion

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Preserve existing RawRepresentationFactory when disabling stored output

Address PR review feedback: wrap/chain the existing factory instead of
replacing it, so upstream configuration (e.g., deploymentName/model defaults
from AsIChatClient) is preserved.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 11:19:32 +00:00
SergeyMenshykhandGitHub 6626565f7a ADR to support a multi-source architecture for agent skills (#4787)
* add  adr suggesting a new design to support a multi-source architecture for agent skills

* add deciders

* move the adr to the decisions folder

* remove unnecessary section

* describe adding a custom skill source

* update

* address comments

* add constructor overloads to inline skill resource and script

* consider ai-function as an alternative for skill script and skill resource model classes

* update decision outcome section and sync adr with latest changes in the code
2026-03-26 11:19:09 +00:00
westeyandGitHub bfda595e56 Add ADR to decide consistency of Chat History Persistence (#4816)
* Add ADR to decide consitency of Chat History Persistence

* Add example

* Update ADR with review results

* Remove unecessary clarification

* Rename ADR to no 22
2026-03-26 11:10:33 +00:00
efb14cedb1 Python: Support structuredContent in MCP tool results and fix sampling options type (#4763)
* Support MCP sampling tools capability (#4625)

Forward systemPrompt, tools, and toolChoice from MCP sampling requests
to the chat client's get_response() call. Also advertise the
sampling.tools capability to MCP servers when a client is configured.

- Pass SamplingCapability with tools support to ClientSession
- Convert systemPrompt to instructions in options
- Convert MCP Tool objects to FunctionTool instances for options
- Map MCP ToolChoice.mode to tool_choice in options
- Add tests for all new behaviors and update existing sampling tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix #4625: Support MCP sampling tool with proper typing and structured content

- Fix mypy error by typing sampling callback options as ChatOptions[None]
  instead of dict[str, Any], and importing ChatOptions from _types
- Handle structuredContent from CallToolResult in _parse_tool_result_from_mcp,
  serializing it as JSON text Content when present
- Add tests for structuredContent parsing (with and without regular content)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix lint: add author to TODO comment

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4625: remove default=str, add edge-case tests

- Remove default=str from json.dumps for structuredContent to fail fast
  on non-JSON-serializable values instead of silently converting
- Add test for non-JSON-serializable structuredContent (TypeError)
- Add tests for empty systemPrompt ('') and empty tools list ([]) edge
  cases in sampling callback
- Expand TODO comment noting list[Content] return type constraint for
  future result_type support

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Sanitize sampling callback error to avoid leaking internals (#4625)

Log exception details at DEBUG level instead of including them in the
ErrorData message returned to the MCP server, which may be untrusted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4625: move params to options, restore error info

- Remove stale TODO comment about response_format (ChatOptions already has it)
- Restore {ex} in sampling callback error message for useful debugging info
- Set structuredContent as additional_property on Content for structured access
- Move temperature, max_tokens, stop into options dict (not top-level kwargs)
- Only set temperature when provided (not all models support it)
- Add tests for generation params in options and temperature omission

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix MCP sampling callback and structured content error handling (#4625)

- Guard max_tokens like temperature: only set when not None, so options
  can properly evaluate to None when all params are absent
- Wrap json.dumps of structuredContent in try/except to fall back to
  str() for non-serializable values instead of propagating TypeError
- Extract test_connect_sampling_capabilities_with_client into its own
  test function so pytest can discover it independently
- Add test for max_tokens=None omission from options
- Update structured content non-serializable test to expect fallback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4625: review comment fixes

* Fix MCP and Azure validation regressions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 07:33:19 +00:00
dd3d085539 Python: Include reasoning messages in MESSAGES_SNAPSHOT events (#4844)
* Include reasoning messages in MESSAGES_SNAPSHOT (#4843)

FlowState now tracks reasoning messages emitted during a run.
_emit_text_reasoning() persists reasoning (including encrypted_value)
into flow.reasoning_messages, and _build_messages_snapshot() appends
them to the final MESSAGES_SNAPSHOT event.

Changes:
- Add reasoning_messages field to FlowState
- Update _emit_text_reasoning() to accept optional flow parameter
- Include reasoning_messages in _build_messages_snapshot()
- Add 'reasoning' to ALLOWED_AGUI_ROLES so normalize_agui_role()
  preserves the role through snapshot round-trips
- Skip reasoning messages in agui_messages_to_agent_framework() since
  they are UI-only state and should not be forwarded to LLM providers
- Add regression tests for snapshot emission, encrypted value
  preservation, and multi-turn round-trip with reasoning

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Include reasoning messages in MESSAGES_SNAPSHOT events

Fixes #4843

* Fix PR review feedback for reasoning persistence (#4843)

- Accumulate reasoning text per message_id (append deltas) instead of
  storing only the current chunk, matching flow.accumulated_text pattern
- Use camelCase encryptedValue in snapshot JSON to match AG-UI protocol
  conventions (toolCallId, encryptedValue)
- Normalize snake_case encrypted_value to encryptedValue in
  agui_messages_to_snapshot_format for input compatibility
- Update normalize_agui_role docstring to include reasoning role
- Add tests for incremental reasoning accumulation and key normalization

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4843: Python: agent-framework-ag-ui: include reasoning messages in MESSAGES_SNAPSHOT

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 05:56:10 +00:00
dc27740f1a Python: Fix streaming path to emit mcp_server_tool_result on output_item.done instead of output_item.added (#4821)
* Fix streaming path to deliver mcp_server_tool_result content (#4814)

Remove premature mcp_server_tool_result emission from the
response.output_item.added/mcp_call handler — at that point the MCP
server has not yet responded and output is always None.

Add a handler for response.mcp_call.completed that emits
mcp_server_tool_result with the actual tool output, matching the
non-streaming path behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix streaming path to deliver mcp_server_tool_result content (#4814)

Stop eagerly emitting mcp_server_tool_result on response.output_item.added
(when output is always None). Instead, handle response.output_item.done for
mcp_call items, which carries the full McpCall with populated output.

This matches the non-streaming path which guards with 'if item.output is not
None' before emitting the result.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix test docstring to match actual implementation event name

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review: call_id fallback and raw_representation consistency (#4814)

- Add call_id fallback in response.output_item.done mcp_call handler to
  match the output_item.added handler pattern
- Use done_item instead of event for raw_representation to keep
  consistent with other output_item branches and non-streaming path
- Add test for call_id fallback when id attribute is missing
- Add raw_representation assertions to existing done handler tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review: call_id fallback for non-streaming path and test coverage (#4814)

- Apply defensive call_id fallback (getattr with id/call_id/empty) to
  non-streaming mcp_call path for consistency with streaming path
- Add raw_representation assertion to call_id fallback test
- Add test for empty-string fallback when neither id nor call_id exist

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 02:09:22 +00:00
c1435ac201 Python: Fix A2AAgent to surface message content from in-progress TaskStatusUpdateEvents (#4798)
* Fix A2AAgent dropping message content from in-progress TaskStatusUpdateEvents (#4783)

_updates_from_task() returned [] for working-state tasks when
background=False, silently discarding all intermediate message content
from task.status.message. Now extracts and yields message parts from
in-progress status updates during streaming.

Also fixed MockA2AClient.send_message to yield all queued responses
(enabling multi-event streaming tests) and added text parameter to
add_in_progress_task_response for tests that need status messages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix: gate intermediate status updates behind emit_intermediate flag and add missing test coverage

- Add emit_intermediate parameter to _updates_from_task and _map_a2a_stream
- Thread stream flag from run() so only streaming callers see intermediate updates
- Add IN_PROGRESS_TASK_STATES guard to emit_intermediate condition
- Add role parameter to test helper add_in_progress_task_response
- Add clarifying comment on MockA2AClient.send_message batch semantics
- Add tests for user role mapping, background precedence, non-streaming behavior,
  terminal task with no artifacts, and empty parts edge case

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-26 02:08:47 +00:00
Jacob AlberandGitHub 0bdcaa5c07 fix: Re-enable the "retrieve object" check in StateManager (#1881) 2026-03-25 22:53:44 +00:00
Jacob AlberandGitHub 35f44e854e .NET: fix: HandoffAgentExecutor does not output any response when non-streaming (#4745)
* fix: HandoffAgentExecutor does not output any reponse when non-streaming

* fix: Ensure Workflow outputs persisted in chat history when hosted AsAgent

* fix: Remove duplicate history entry creation and ad test

* test: Add streaming tests for AsAgent to smoke tests

* feat: Add output configurability to Handoffs
2026-03-25 22:22:13 +00:00
Jacob AlberandGitHub 0756c45702 .NET: [BREAKING] Update type names and source generator to reduce conflicts (#4903)
* refactor: [BREAKING] Config => ExecutorConfig

Make the Config name less likely to collide with other classes by renaming to ExecutorConfig. Makes Configured and related classes internal as they do not need to be part of the public surface.

* fix: Make RouteBuilder explicit in SourceGen to avoid conflicts
2026-03-25 20:35:17 +00:00
0b2ccd6126 .NET: Fix ChatOptions mutation in AIContextProviderChatClient across calls (#4891)
* Fix UseAIContextProviders tool accumulation across calls (#4864)

Clone ChatOptions before mutating it in InvokeProvidersAsync to prevent
context provider tools from accumulating when the same ChatOptions
instance is reused across multiple API calls.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: chetantoshniwal <chetantoshniwal@gmail.com>

* Apply suggestion from @westey-m

---------

Co-authored-by: MAF Dashboard Bot <maf-dashboard-bot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
2026-03-25 20:02:16 +00:00
Peter IbekweandGitHub 23921c0f6e .NET: Pass through external input request and handle response conversion for workflow as agent scenario (#4361)
* Handle external input request and response conversion for workflow as agent scenario

* Remove unnecessary test comment

* Fix PR comments

* Updated to fix edge cases, and add more tests.

* Update pending requests to use typed properties instead of relying on StateBag. replying to PR feedback.

* Fixed external response de-dup and updated possible brittle test.

* Address PR comments on sending turn token for normal messages and handle contentId collision by source agent

* Remove unnecessary serialization element and address pr comment on intercepted outgoing requests

* Updated MEAI changes for UserInput request and response abstractions.
2026-03-25 19:23:44 +00:00
db16a9e74c .NET: Clarify IResettableExecutor usage comment in workflow sample (#4905)
* Clarify IResettableExecutor usage comment in workflow sample

* Update dotnet/samples/03-workflows/Agents/WorkflowAsAnAgent/WorkflowFactory.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dotnet/samples/03-workflows/Agents/WorkflowAsAnAgent/WorkflowFactory.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-25 18:50:29 +00:00
Eduard van ValkenburgandGitHub c012aac5f2 Python: [BREAKING] Reduce core dependencies and simplify optional integrations (#4904)
* improved dependencies and some fixes

* fix for mypy

* improve mcp
2026-03-25 18:03:43 +00:00
Shyju KrishnankuttyandGitHub 49d69b3bf5 .NET: Expose workflows as MCP tools when hosting on Azure functions (#4768)
* Expose workflow as MCP Tool

* Expose workflow as MCP Tool

* Cleanup

* PR feedback fixes

* update changelog to include PR numner

* Improvements to error handling.

* Adding a sample project demonstrating how to setup Agents and Workflows together.

* Ensure duplicate agent registrations are properly handled.
2026-03-25 15:43:15 +00:00
Jacob AlberandGitHub a9db40886e fix: FS Checkpoint storage special character support (#4730)
The `sessionId`, an optional parameter when starting a new session when
running a workflow is an arbitrary string. This allows consumers to
support whatever ids are needed by other systems, but can result in
errors when an OS special or forbidden character is included.

The fix is to escape the paths, in a 1:1 manner. We rely on
EncodeDataString to do this.

* Also modifies the index file to make it easier to determine what the
  name of the file on disk is for a given `sessionId`.
2026-03-25 15:21:30 +00:00
westeyandGitHub 87962e53c5 .NET: Persist messages during function call loop (#4762)
* Persist messages during the Function Call Loop

* Revert version reset

* Fix bugs and improve sample

* Fix formatting issues

* Also updating conversation id during run

* Update based on ADR feedback
2026-03-25 11:53:45 +00:00
5e056b672e Python: [BREAKING] Python: Provider-leading client design & OpenAI package extraction (#4818)
* Python: Provider-leading client design & OpenAI package extraction

Major refactoring of the Python Agent Framework client architecture:

- Extract OpenAI clients into new `agent-framework-openai` package
- Core package no longer depends on openai, azure-identity, azure-ai-projects
- Rename clients for discoverability: OpenAIResponsesClient → OpenAIChatClient,
  OpenAIChatClient → OpenAIChatCompletionClient
- Unify `model_id`/`deployment_name`/`model_deployment_name` → `model` param
- New FoundryChatClient for Azure AI Foundry Responses API
- New FoundryAgent/FoundryAgentClient for connecting to pre-configured Foundry agents
- Remove OpenAIBase/OpenAIConfigMixin from non-deprecated client MRO
- Deprecate AzureOpenAI* clients, AzureAIClient, OpenAIAssistantsClient
- Reorganize samples: azure_openai+azure_ai+azure_ai_agent → azure/
- ADR-0020: Provider-Leading Client Design

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: missing Agent imports in samples, .model_id → .model in foundry_local sample

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: CI failures — mypy errors, coverage targets, sample imports

- azure-ai mypy: add type ignores for TypedDict total=, model arg, forward ref
- Coverage: replace core.azure/openai targets with openai package target
- project_provider: add type annotation for opts dict

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: populate openai .pyi stub, fix broken README links, coverage targets

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fixes

* updated observabilitty

* reset azure init.pyi

* fix errors

* updated adr number

* fix foundry local

* fixed not renamed docstrings and comments, and added deprecated markers to old classes

* fix tests and pyprojects

* fix test vars

* updated function tests

* update durable

* updated test setup for functions

* Fix Foundry auth in workflow samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Stabilize Python integration workflows

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update hosting samples for Foundry

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Trigger full CI rerun

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Trigger CI rerun again

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* trigger rerun

* trigger rerun

* fix for litellm

* undo durabletask changes

* Move Foundry APIs into foundry namespace

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Foundry pyproject formatting

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Split provider samples by Foundry surface

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Restore hosting sample requirements

Also fix the Foundry Local sample link after the provider sample move.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* updated tests

* udpated foundry integration tests

* removed dist from azurefunctions tests

* Use separate Foundry clients for concurrent agents

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix client setup in azfunc and durable

* disabled two tests

* updated setup for some function and durable tests

* improved azure openai setup with new clients

* ignore deprecated

* fixes

* skip 11

* remove openai assistants int tests

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-25 09:56:29 +00:00
Tao ChenandGitHub 4b533608b6 Python: Update sample validation scripts (#4870)
* Update sample validation scripts

* Adjust prompt

* Update autogen-migration samples

* Add fix suggestion

* Split jobs

* Add .env

* Create trend report

* Add timestamp

* Add more env vars

* Comments

* force node24

* force node24

* force node22
2026-03-25 01:21:32 +00:00
westeyandGitHub 2c000b032d .NET: Update AIContextProviders to use Microsoft.Extensions.Compliance.Redaction (#4854)
* Update providers to use Microsoft.Extensions.Compliance.Redaction

* Fix formatting.

* Fix readme
2026-03-24 18:12:55 +00:00
cc85bbc2dc .NET: Re-enable AzureAI.Persistent packaging and integration tests (#4769) (#4865)
Azure.AI.Agents.Persistent 1.2.0-beta.10 now targets ME.AI 10.4.0+,
resolving the compatibility issue that required disabling this package.

- Remove IsPackable=false from the csproj
- Re-enable all 6 integration test classes (IntegrationDisabled → Integration)
- Remove outdated compatibility warning from README.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 11:06:32 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
01aaf2baea Bump actions/download-artifact from 7 to 8 (#4372)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 21:55:19 +00:00
cb96347c95 Python: Fix PydanticSchemaGenerationError when using from __future__ import annotations with @tool (#4822)
* Fix PydanticSchemaGenerationError with PEP 563 annotations in @tool

_resolve_input_model used raw param.annotation from inspect.signature(),
which returns string annotations when 'from __future__ import annotations'
is active (PEP 563). This caused Pydantic's create_model to fail for
complex types like Optional[int] or FunctionInvocationContext.

Use typing.get_type_hints() to resolve annotations to actual types before
passing them to create_model, matching the approach already used by
_discover_injected_parameters.

Fixes #4809

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Apply pre-commit auto-fixes

* Remove reproduction report and unused test imports

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(tests): strengthen PEP 563 regression tests per review feedback (#4809)

- Verify type correctness in schema assertions (not just key presence)
- Fix ctx annotation to FunctionInvocationContext | None for type consistency
- Add test for Optional[CustomType] pattern (original bug trigger)
- Add test for get_type_hints() fallback with unresolvable forward refs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Apply pre-commit auto-fixes

* Address review feedback for #4809: Python: [Bug]: PydanticSchemaGenerationError in FunctionInvocationContext

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 21:47:12 +00:00
westeyandGitHub 5070c67d0e Mark constructors of InvokingContext and InvokedContext as experimental (#4851) 2026-03-23 19:06:07 +00:00
9a47620f64 .NET: Update Hosted Samples References to latest beta.11 (#4853)
* Bump HostedAgents samples to AgentFramework beta.11 and pass credential to UseFoundryTools

Update all 8 HostedAgents samples:
- Azure.AI.AgentServer.AgentFramework -> 1.0.0-beta.11
- Microsoft.Agents.AI.OpenAI -> 1.0.0-rc4
- Microsoft.Agents.AI/AzureAI/Workflows -> 1.0.0-rc4
- Azure.AI.Projects -> 2.0.0-beta.1
- Fix Workflow.AsAgent() -> AsAIAgent() in FoundryMultiAgent
- Pass credential to UseFoundryTools in AgentWithTools (resolves #56802)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove AgentWithTools sample (UseFoundryTools no longer supported)

Remove the AgentWithTools hosted agent sample as the UseFoundryTools
backend is no longer supported. Updated HostedAgents README and solution
file to remove all references.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix AgentWithHostedMCP: downgrade Azure.AI.OpenAI to 2.8.0-beta.1 for rc4 compatibility

Azure.AI.OpenAI 2.9.0-beta.1 has breaking changes (GetResponsesClient no
longer accepts deployment name, ResponsesClient.Model removed) that are
incompatible with Microsoft.Agents.AI.OpenAI rc4. Pin to 2.8.0-beta.1 and
use GetResponsesClient(deploymentName).AsAIAgent() pattern.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 17:41:49 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e11633a2c8 Bump Azure.AI.Agents.Persistent from 1.2.0-beta.9 to 1.2.0-beta.10 (#4833)
---
updated-dependencies:
- dependency-name: Azure.AI.Agents.Persistent
  dependency-version: 1.2.0-beta.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 15:26:40 +00:00
Peter IbekweandGitHub 7e6d87e7ec .NET: Fix broken workflow samples (#4800)
* Fix source generator bug that silently drops base class handler registrations for protocol-only partial executors

* Fixed xml comments and variable naming.

* Fix workflow samples broken due to routing change
2026-03-20 20:49:30 +00:00
9dfe7c40ca Add ADR-0020: Foundry Evals integration (#4731)
* Add ADR-0020: Foundry Evals integration design

Captures the design for integrating Azure AI Foundry Evaluations with
agent-framework. Key decisions:

- EvalItem with conversation (list[Message]) as single source of truth
- query/response derived from configurable conversation split strategies
- Tools as list[FunctionTool] (including auto-extracted MCP tools)
- FoundryEvals provider with auto-detection of evaluator capabilities
- LocalEvaluator with @function_evaluator decorator for local checks
- Consistent Python/C# APIs: evaluate_agent, evaluate_workflow

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Mark ADR 0020 Foundry Evals as accepted

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 20:20:59 +00:00
westeyandGitHub 6803058e36 .NET: Obsolete the V1 helper methods and migrate samples using it where possible (#4795)
* Obsolete the V1 helper methods and migrate samples using it where possible

* Address PR comments
2026-03-20 19:41:34 +00:00
westeyandGitHub 7645ec4e07 .NET: Improve visibility for AzureFunctions Workflows samples run tests in increase timeouts (#4820)
* Reduce timeout flakiness for AzureFunctions Workflows samples run tests

* Add more updates

* Address PR comments

* Address PR comments
2026-03-20 18:41:30 +00:00
Tao ChenandGitHub 51828abed4 [BREAKING] Python: Add context mode to AgentExecutor (#4668)
* Add context mode to AgentExecutor

* Fix unit tests

* Address comments

* Address comments

* REvise context mode and add tests

* Add chain config to sequential builder

* Add sample

* Fix pipeline

* Address comments

* Address comments
2026-03-20 18:27:02 +00:00
CopilotGitHubCopilotcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>stephentoubrogerbarreto
88ea9d08c7 .NET: Update to OpenAI 2.9.1, Azure.AI.OpenAI 2.9.0-beta.1, Microsoft.Extensions.AI 10.4.0, and Azure.AI.Projects 2.0.0-beta.2 (#4613)
* Initial plan

* Update code for Microsoft.Extensions.AI.Abstractions 10.4.0 breaking changes

- Rename FunctionApprovalRequestContent → ToolApprovalRequestContent
- Rename FunctionApprovalResponseContent → ToolApprovalResponseContent
- Rename UserInputRequestContent → ToolApprovalRequestContent
- Rename UserInputResponseContent → ToolApprovalResponseContent
- Update .FunctionCall property → .ToolCall with FunctionCallContent casts where needed
- Update .Id property → .RequestId on the renamed types
- Rename FunctionApprovalRequestEventGenerator → ToolApprovalRequestEventGenerator
- Rename FunctionApprovalResponseEventGenerator → ToolApprovalResponseEventGenerator

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update OpenAI 2.9.1, ME.AI 10.4.0, fix breaking API changes

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Fix remaining ME.AI 10.4.0 breaking changes: MCP approval types, .Output→.Outputs

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Use pattern matching with `when` for ToolApprovalRequestContent/FunctionCallContent

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Update Azure.AI.OpenAI to 2.9.0-beta.1

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Fix remaining GetResponsesClient(model) build failures for Azure.AI.OpenAI 2.9.0-beta.1

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Address review feedback: remove redundant type checks in TestRequestAgent.cs and fix error message in AIAgentHostExecutor.cs

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Update Azure.AI.Projects to 2.0.0-beta.2 with namespace migration

- Azure.AI.Projects 2.0.0-beta.1 → 2.0.0-beta.2
- Azure.AI.Projects.OpenAI → Azure.AI.Extensions.OpenAI (transitive)
- Agent types moved to Azure.AI.Projects.Agents namespace
- AgentRecord.Versions.Latest → AgentRecord.GetLatestVersion()
- OpenAPIFunctionDefinition → OpenApiFunctionDefinition
- BingCustomSearchToolParameters → BingCustomSearchToolOptions
- MemorySearchPreviewTool.UpdateDelay → UpdateDelayInSecs
- Azure.Identity 1.17.1 → 1.19.0
- Microsoft.Identity.Client.Extensions.Msal 4.78.0 → 4.83.1

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Fix remaining type renames for Azure.AI.Projects 2.0.0-beta.2

- BrowserAutomationToolParameters → BrowserAutomationToolOptions
- MemoryUpdateOptions.UpdateDelay stays as UpdateDelay (not renamed)
- WaitForMemoriesUpdateAsync parameter order: pollingInterval before options
- AIProjectAgentsOperations → AgentsClient

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Fix format errors and OpenTelemetry test for ME.AI 10.4.0

- Remove unused 'using Azure.AI.Extensions.OpenAI' and fix import ordering
  in Agent_With_AzureAIProject/Program.cs
- Update OpenTelemetryAgentTests: gen_ai.tool.definitions is now always
  emitted regardless of EnableSensitiveData per ME.AI 10.4.0 change
  (dotnet/extensions#7346). Tool definitions are not considered sensitive.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix GetRepoFolder() to work in git worktrees

Use 'workflow-samples' directory as repo root marker instead of '.git',
which fails in worktrees (.git is a file) and also matches too early
when a '.github' folder exists in subdirectories.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix formatting: remove unused usings and fix import ordering

dotnet format applied across 59 impacted projects. Primarily removes
unnecessary 'using Azure.AI.Projects' where Azure.AI.Projects.Agents
provides all needed types, and fixes import ordering per editorconfig.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Disable AzureAIAgentsPersistent integration tests (#4769)

Azure.AI.Agents.Persistent 1.2.0-beta.9 references McpServerToolApprovalResponseContent
which was removed in ME.AI 10.4.0 (renamed to ToolApprovalResponseContent), causing
TypeLoadException at runtime. Mark all 6 test classes with IntegrationDisabled trait
until Persistent ships a version targeting ME.AI 10.4.0+.

Upstream fix: https://github.com/Azure/azure-sdk-for-net/pull/56929

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add README with compatibility note for AzureAI.Persistent (#4769)

Documents that Azure.AI.Agents.Persistent 1.2.0-beta.9 is only compatible
with ME.AI ≤10.3.0 and OpenAI ≤2.8.0 due to type renames in ME.AI 10.4.0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix file encoding: restore UTF-8 BOM on Persistent test files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Mark AzureAI.Persistent as IsPackable=false (#4769)

Prevent shipping until Azure.AI.Agents.Persistent targets ME.AI 10.4.0+.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Moving IsPackable after import

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
2026-03-20 14:29:29 +00:00
8edcb282f4 Update script to ping only on waiting-for-author label (#4812)
* update script to ping only on certain waiting for author label

* Update .github/scripts/stale_issue_pr_ping.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update .github/scripts/stale_issue_pr_ping.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix docstring

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-20 19:39:22 +09:00
81e2336d47 Python: avoid duplicate agent response telemetry (#4685)
* Python: avoid duplicate agent response telemetry

* Python: conditionally suppress duplicate agent telemetry

* Simplify telemetry ownership tracking

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Aggregate token usage from inner chat spans on invoke_agent span

The invoke_agent span now carries the aggregated input/output token
counts from all inner chat completion spans that occur during an agent
run. Previously, when inner ChatTelemetryLayer spans captured usage,
the outer AgentTelemetryLayer skipped setting usage entirely to avoid
duplication. Now a new INNER_ACCUMULATED_USAGE context variable tracks
cumulative usage across all inner completions, and the agent span
always reports the total.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 10:09:46 +00:00
8fc19a3437 Python: Deprecate Azure AI v1 (Persistent Agents API) helper methods (#4804)
* Deprecate Azure AI v1 (Persistent Agents API) helper methods

Add DeprecationWarning to v1 classes and functions that have been
superseded by the v2 (Projects/Responses) API:

- AzureAIAgentsProvider -> use AzureAIProjectAgentProvider
- AzureAIAgentClient -> use AzureAIClient
- AzureAIAgentOptions -> use AzureAIProjectAgentOptions
- to_azure_ai_agent_tools() -> use to_azure_ai_tools()
- from_azure_ai_agent_tools() -> use from_azure_ai_tools()
- AzureAIAgentClient static tool factory methods -> use AzureAIClient equivalents

All v1 components still function but emit warnings to guide migration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add deprecation warnings to AzureAIAgentsProvider methods

Mark create_agent(), get_agent(), and as_agent() as deprecated
individually, pointing to AzureAIProjectAgentProvider equivalents.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 08:53:36 +00:00
2040 changed files with 122101 additions and 60742 deletions
+1
View File
@@ -21,6 +21,7 @@ ignorePatterns:
- pattern: "http://host.docker.internal"
- pattern: "https://openai.github.io/openai-agents-js/openai/agents/classes/"
- pattern: "https:\/\/dotnet.microsoft.com\/download"
- pattern: "https://github.com/Rel1cx/eslint-react"
# excludedDirs:
# Folders which include links to localhost, since it's not ignored with regular expressions
baseUrl: https://github.com/microsoft/agent-framework/
+1 -1
View File
@@ -47,7 +47,7 @@ body:
attributes:
label: Package Versions
description: List the agent-framework-* packages and versions you are using
placeholder: "e.g., agent-framework-core: 1.0.0, agent-framework-azure-ai: 1.0.0"
placeholder: "e.g., agent-framework-core: 1.0.0, agent-framework-foundry: 1.0.0"
validations:
required: true
@@ -24,7 +24,9 @@ runs:
using: "composite"
steps:
- name: Set up Node.js environment
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 22
- name: Install Copilot CLI
shell: bash
@@ -32,7 +34,7 @@ runs:
- name: Test Copilot CLI
shell: bash
run: copilot -p "What can you do in one sentence?"
run: copilot --version && copilot -p "What can you do in one sentence?"
- name: Azure CLI Login
uses: azure/login@v2
@@ -0,0 +1,166 @@
name: Setup Local MCP Server
description: Start and validate a local streamable HTTP MCP server for integration tests
inputs:
fallback_url:
description: Existing LOCAL_MCP_URL value to keep as a fallback if local startup fails
required: false
default: ''
host:
description: Host interface to bind the local MCP server
required: false
default: '127.0.0.1'
port:
description: Port to bind the local MCP server
required: false
default: '8011'
mount_path:
description: Mount path for the local streamable HTTP MCP endpoint
required: false
default: '/mcp'
outputs:
effective_url:
description: Local MCP URL when startup succeeds, otherwise the provided fallback URL
value: ${{ steps.start.outputs.effective_url }}
local_url:
description: URL of the local MCP server
value: ${{ steps.start.outputs.local_url }}
started:
description: Whether the local MCP server started and passed validation
value: ${{ steps.start.outputs.started }}
pid:
description: PID of the local MCP server process when startup succeeded
value: ${{ steps.start.outputs.pid }}
runs:
using: composite
steps:
- name: Start and validate local MCP server
id: start
shell: bash
run: |
set -euo pipefail
host="${{ inputs.host }}"
port="${{ inputs.port }}"
mount_path="${{ inputs.mount_path }}"
fallback_url="${{ inputs.fallback_url }}"
if [[ ! "$mount_path" =~ ^/ ]]; then
mount_path="/$mount_path"
fi
local_url="http://${host}:${port}${mount_path}"
health_url="http://${host}:${port}/healthz"
log_file="$RUNNER_TEMP/local-mcp-server.log"
pid_file="$RUNNER_TEMP/local-mcp-server.pid"
rm -f "$log_file" "$pid_file"
server_pid="$(
python3 - "$GITHUB_WORKSPACE/python" "$log_file" "$host" "$port" "$mount_path" <<'PY'
from __future__ import annotations
import subprocess
import sys
workspace, log_file, host, port, mount_path = sys.argv[1:]
with open(log_file, "w", encoding="utf-8") as log:
process = subprocess.Popen(
[
"uv",
"run",
"python",
"scripts/local_mcp_streamable_http_server.py",
"--host",
host,
"--port",
port,
"--mount-path",
mount_path,
],
cwd=workspace,
stdout=log,
stderr=subprocess.STDOUT,
start_new_session=True,
)
print(process.pid)
PY
)"
echo "$server_pid" > "$pid_file"
started=false
for _ in $(seq 1 30); do
if curl --silent --fail "$health_url" >/dev/null; then
started=true
break
fi
if ! kill -0 "$server_pid" 2>/dev/null; then
break
fi
sleep 1
done
if [[ "$started" == "true" ]]; then
if ! (
cd "$GITHUB_WORKSPACE/python"
LOCAL_MCP_URL="$local_url" uv run python - <<'PY'
from __future__ import annotations
import asyncio
import os
from agent_framework import Content, MCPStreamableHTTPTool
def result_to_text(result: str | list[Content]) -> str:
if isinstance(result, str):
return result
return "\n".join(content.text for content in result if content.type == "text" and content.text)
async def main() -> None:
tool = MCPStreamableHTTPTool(
name="local_ci_mcp",
url=os.environ["LOCAL_MCP_URL"],
approval_mode="never_require",
)
async with tool:
assert tool.functions, "Local MCP server did not expose any tools."
result = result_to_text(await tool.functions[0].invoke(query="What is Agent Framework?"))
assert result, "Local MCP server returned an empty response."
asyncio.run(main())
PY
); then
started=false
fi
fi
effective_url="$local_url"
pid="$server_pid"
if [[ "$started" != "true" ]]; then
effective_url="$fallback_url"
pid=""
if kill -0 "$server_pid" 2>/dev/null; then
kill -TERM -- "-$server_pid" 2>/dev/null || kill -TERM "$server_pid" || true
sleep 1
kill -KILL -- "-$server_pid" 2>/dev/null || kill -KILL "$server_pid" || true
fi
echo "Local MCP server was unavailable; continuing with fallback LOCAL_MCP_URL."
if [[ -f "$log_file" ]]; then
tail -n 100 "$log_file" || true
fi
else
echo "Using local MCP server at $local_url"
fi
echo "started=$started" >> "$GITHUB_OUTPUT"
echo "local_url=$local_url" >> "$GITHUB_OUTPUT"
echo "effective_url=$effective_url" >> "$GITHUB_OUTPUT"
echo "pid=$pid" >> "$GITHUB_OUTPUT"
+20 -11
View File
@@ -1,9 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
"""Scan open issues and PRs for stale follow-ups from external authors.
"""Scan open issues and PRs labeled 'waiting-for-author' for stale follow-ups.
If a team member commented and the external author hasn't replied within
DAYS_THRESHOLD days, post a reminder comment and add the 'needs-info' label.
Team members manually add the 'waiting-for-author' label when they need a
response from the external author. If the author hasn't replied within
DAYS_THRESHOLD days of the last team comment, post a reminder and add the
'requested-info' label to prevent duplicate pings.
"""
from __future__ import annotations
@@ -22,7 +24,8 @@ PING_COMMENT = (
"@{author}, friendly reminder — this issue is waiting on your response. "
"Please share any updates when you get a chance. (This is an automated message.)"
)
LABEL = "needs-info"
TRIGGER_LABEL = "waiting-for-author"
PINGED_LABEL = "requested-info"
def get_team_members(g: Github, org: str, team_slug: str) -> set[str]:
@@ -76,15 +79,21 @@ def should_ping(
days_threshold: int,
now: datetime,
) -> bool:
"""Determine whether this issue/PR should be pinged."""
"""Determine whether this issue/PR should be pinged.
Only issues/PRs carrying the 'waiting-for-author' label are candidates.
"""
author = issue.user.login
# Skip if the trigger label is not present
if not any(label.name == TRIGGER_LABEL for label in issue.labels):
return False
# Skip if author is a team member
if author in team_members:
return False
# Skip if already labeled
if any(label.name == LABEL for label in issue.labels):
# Skip if already pinged
if any(label.name == PINGED_LABEL for label in issue.labels):
return False
# Skip if no comments at all
@@ -112,7 +121,7 @@ def should_ping(
def ping(issue: Issue, dry_run: bool) -> bool:
"""Post a reminder comment and add the needs-info label. Returns True on success."""
"""Post a reminder comment and add the 'requested-info' label. Returns True on success."""
author = issue.user.login
kind = "PR" if issue.pull_request else "Issue"
@@ -129,7 +138,7 @@ def ping(issue: Issue, dry_run: bool) -> bool:
issue.create_comment(PING_COMMENT.format(author=author))
commented = True
if not labeled:
issue.add_to_labels(LABEL)
issue.add_to_labels(PINGED_LABEL)
labeled = True
print(f" Pinged {kind} #{issue.number} (@{author})")
return True
@@ -184,9 +193,9 @@ def main() -> None:
failed = []
scanned = 0
print(f"Scanning open issues and PRs (threshold: {days_threshold} days)...\n")
print(f"Scanning open issues and PRs labeled '{TRIGGER_LABEL}' (threshold: {days_threshold} days)...\n")
for issue in repo.get_issues(state="open"):
for issue in repo.get_issues(state="open", labels=[TRIGGER_LABEL]):
scanned += 1
if should_ping(issue, team_members, days_threshold, now):
+10 -6
View File
@@ -15,8 +15,9 @@ import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
from stale_issue_pr_ping import (
LABEL,
PINGED_LABEL,
PING_COMMENT,
TRIGGER_LABEL,
author_replied_after,
find_last_team_comment,
get_team_members,
@@ -63,7 +64,10 @@ def _make_issue(
issue.user = MagicMock()
issue.user.login = author
issue.number = number
issue.labels = [_make_label(n) for n in (labels or [])]
# Default to having the trigger label, since the API query pre-filters.
if labels is None:
labels = [TRIGGER_LABEL]
issue.labels = [_make_label(n) for n in labels]
issue.comments = comment_count
issue.pull_request = MagicMock() if pull_request else None
if comments is not None:
@@ -136,11 +140,11 @@ class TestShouldPing:
assert should_ping(issue, TEAM, 4, NOW) is True
def test_skip_team_member_author(self):
issue = _make_issue(author="alice", comment_count=1)
issue = _make_issue(author="alice", labels=[TRIGGER_LABEL], comment_count=1)
assert should_ping(issue, TEAM, 4, NOW) is False
def test_skip_already_labeled(self):
issue = _make_issue(labels=[LABEL], comment_count=1)
def test_skip_already_pinged(self):
issue = _make_issue(labels=[TRIGGER_LABEL, PINGED_LABEL], comment_count=1)
assert should_ping(issue, TEAM, 4, NOW) is False
def test_skip_no_comments(self):
@@ -194,7 +198,7 @@ class TestPing:
issue = _make_issue()
assert ping(issue, dry_run=False) is True
issue.create_comment.assert_called_once()
issue.add_to_labels.assert_called_once_with(LABEL)
issue.add_to_labels.assert_called_once_with(PINGED_LABEL)
@patch("stale_issue_pr_ping.time.sleep")
def test_retry_on_failure(self, mock_sleep):
+165
View File
@@ -0,0 +1,165 @@
name: DevFlow PR Review
on:
pull_request_target:
types:
- opened
- reopened
- ready_for_review
workflow_dispatch:
inputs:
pr_number:
description: Pull request number to review
required: true
type: string
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: devflow-pr-review-${{ github.repository }}-${{ github.event.pull_request.number || inputs.pr_number || github.run_id }}
cancel-in-progress: true
env:
DEVFLOW_REPOSITORY: ${{ vars.DF_REPO }}
DEVFLOW_REF: main
TARGET_REPO_PATH: ${{ github.workspace }}/target-repo
DEVFLOW_PATH: ${{ github.workspace }}/devflow
jobs:
team_check:
runs-on: ubuntu-latest
outputs:
is_team_member: ${{ steps.check.outputs.is_team_member }}
pr_number: ${{ steps.pr.outputs.pr_number }}
pr_url: ${{ steps.pr.outputs.pr_url }}
repo: ${{ steps.pr.outputs.repo }}
steps:
- name: Resolve PR metadata
id: pr
shell: bash
env:
PR_HTML_URL: ${{ github.event.pull_request.html_url }}
PR_NUMBER_EVENT: ${{ github.event.pull_request.number }}
PR_NUMBER_INPUT: ${{ inputs.pr_number }}
run: |
set -euo pipefail
if [[ "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then
pr_number="${PR_NUMBER_EVENT}"
pr_url="${PR_HTML_URL}"
else
pr_number="${PR_NUMBER_INPUT}"
pr_url="https://github.com/${GITHUB_REPOSITORY}/pull/${pr_number}"
fi
if [[ ! "$pr_number" =~ ^[1-9][0-9]*$ ]]; then
echo "Could not determine PR number; for workflow_dispatch runs, the 'pr_number' input is required when not running on pull_request_target." >&2
exit 1
fi
echo "pr_url=${pr_url}" >> "$GITHUB_OUTPUT"
echo "pr_number=${pr_number}" >> "$GITHUB_OUTPUT"
echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
- name: Check PR author team membership
id: check
uses: actions/github-script@v8
env:
TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }}
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
with:
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
script: |
let author = context.payload.pull_request?.user?.login;
if (!author) {
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: Number(process.env.PR_NUMBER),
});
author = pr.user.login;
}
let isTeamMember = false;
try {
const teamMembership = await github.rest.teams.getMembershipForUserInOrg({
org: context.repo.owner,
team_slug: process.env.TEAM_NAME,
username: author,
});
isTeamMember = teamMembership.data.state === 'active';
} catch (error) {
console.log(`Team membership lookup failed for ${author}: ${error.message}`);
isTeamMember = false;
}
core.setOutput('is_team_member', isTeamMember ? 'true' : 'false');
if (isTeamMember) {
core.info(`Author ${author} is a team member; proceeding with review.`);
} else {
core.info(`Author ${author} is not a member of ${process.env.TEAM_NAME}; skipping review.`);
}
review:
runs-on: ubuntu-latest
needs: team_check
if: ${{ needs.team_check.outputs.is_team_member == 'true' }}
timeout-minutes: 60
# Advisory check: failures here should not block the PR. The reviewer
# posts comments as a best-effort signal; if the pipeline breaks, the
# PR author should still be able to merge without a red required check.
continue-on-error: true
steps:
# Safe checkout: base repo only, not the untrusted PR head.
- name: Checkout target repo base
uses: actions/checkout@v6
with:
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha }}
fetch-depth: 0
persist-credentials: false
path: target-repo
# Private DevFlow checkout: the PAT/token grants access to this repo's code.
- name: Checkout DevFlow
uses: actions/checkout@v6
with:
repository: ${{ env.DEVFLOW_REPOSITORY }}
ref: ${{ env.DEVFLOW_REF }}
token: ${{ secrets.DEVFLOW_TOKEN }}
fetch-depth: 1
persist-credentials: false
path: devflow
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
version: "0.11.x"
enable-cache: true
- name: Install DevFlow dependencies
working-directory: ${{ env.DEVFLOW_PATH }}
run: uv sync --frozen
- name: Run PR review
id: review
working-directory: ${{ env.DEVFLOW_PATH }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_COPILOT_TOKEN: ${{ secrets.GH_COPILOT_TOKEN }}
SK_REPO_PATH: ${{ env.TARGET_REPO_PATH }}
AGENT_REPO_PATH: ${{ env.TARGET_REPO_PATH }}
PR_URL: ${{ needs.team_check.outputs.pr_url }}
run: |
uv run python scripts/trigger_pr_review.py \
--pr-url "$PR_URL" \
--github-username "$GITHUB_ACTOR" \
--no-require-comment-selection
+2 -2
View File
@@ -82,7 +82,7 @@ jobs:
.github
dotnet
python
workflow-samples
declarative-agents
- name: Setup dotnet
uses: actions/setup-dotnet@v5.2.0
@@ -152,7 +152,7 @@ jobs:
.github
dotnet
python
workflow-samples
declarative-agents
# Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened)
- name: Start Azure Cosmos DB Emulator
@@ -38,7 +38,7 @@ jobs:
.github
dotnet
python
workflow-samples
declarative-agents
- name: Start Azure Cosmos DB Emulator
if: runner.os == 'Windows'
+137
View File
@@ -0,0 +1,137 @@
#
# Runs the .NET sample verification tool, which builds and executes sample projects
# and verifies their output using deterministic checks and AI-powered verification.
#
# Results are displayed as a GitHub Job Summary and the CSV report is uploaded as an artifact.
#
name: dotnet-verify-samples
on:
workflow_dispatch:
inputs:
category:
description: "Sample category to run (blank for all)"
required: false
type: choice
options:
- ""
- "01-get-started"
- "02-agents"
- "03-workflows"
parallelism:
description: "Max parallel sample runs"
required: false
default: "8"
type: string
schedule:
- cron: "0 6 * * 1-5" # Weekdays at 6:00 UTC
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
id-token: write
jobs:
verify-samples:
runs-on: ubuntu-latest
environment: 'integration'
timeout-minutes: 90
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
sparse-checkout: |
.
.github
dotnet
python
declarative-agents
- name: Setup dotnet
uses: actions/setup-dotnet@v5.2.0
with:
global-json-file: ${{ github.workspace }}/dotnet/global.json
- name: Azure CLI Login
if: github.event_name != 'pull_request'
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Generate filtered solution
shell: pwsh
run: |
./dotnet/eng/scripts/New-FilteredSolution.ps1 `
-Solution dotnet/agent-framework-dotnet.slnx `
-TargetFramework net10.0 `
-Configuration Debug `
-OutputPath dotnet/filtered.slnx `
-Verbose
- name: Build solution
shell: bash
run: dotnet build dotnet/filtered.slnx -f net10.0 --warnaserror
- name: Run verify-samples
id: verify
working-directory: dotnet
shell: bash
run: |
CATEGORY_ARG=""
if [ -n "$CATEGORY_INPUT" ]; then
CATEGORY_ARG="--category $CATEGORY_INPUT"
fi
dotnet run --project eng/verify-samples -- \
$CATEGORY_ARG \
--parallel "$PARALLELISM" \
--md results.md \
--csv results.csv \
--log results.log
env:
CATEGORY_INPUT: ${{ github.event.inputs.category || '' }}
PARALLELISM: ${{ github.event.inputs.parallelism || '8' }}
# OpenAI Models
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_CHAT_MODEL_NAME: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_REASONING_MODEL_NAME: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
# Azure OpenAI Models
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
# Azure AI Foundry
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_MODEL_DEPLOYMENT_NAME }}
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
- name: Write Job Summary
if: always()
working-directory: dotnet
shell: bash
run: |
if [ -f results.md ]; then
cat results.md >> "$GITHUB_STEP_SUMMARY"
else
echo "⚠️ No results.md generated — verify-samples may have failed to start." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload results
if: always()
uses: actions/upload-artifact@v7
with:
name: verify-samples-results
path: |
dotnet/results.csv
dotnet/results.log
if-no-files-found: warn
- name: Fail if samples failed
if: always() && steps.verify.outcome == 'failure'
shell: bash
run: exit 1
+6 -7
View File
@@ -34,15 +34,14 @@ from dataclasses import dataclass
# (e.g., "packages/core/agent_framework/observability.py")
# =============================================================================
ENFORCED_TARGETS: set[str] = {
# Packages
"packages.azure-ai.agent_framework_azure_ai",
"packages.core.agent_framework",
"packages.core.agent_framework._workflows",
"packages.purview.agent_framework_purview",
# Packages (sorted alphabetically)
"packages.anthropic.agent_framework_anthropic",
"packages.azure-ai-search.agent_framework_azure_ai_search",
"packages.core.agent_framework.azure",
"packages.core.agent_framework.openai",
"packages.core.agent_framework",
"packages.core.agent_framework._workflows",
"packages.foundry.agent_framework_foundry",
"packages.openai.agent_framework_openai",
"packages.purview.agent_framework_purview",
# Individual files (if you want to enforce specific files instead of whole packages)
"packages/core/agent_framework/observability.py",
# Add more targets here as coverage improves
+189 -26
View File
@@ -60,9 +60,10 @@ jobs:
environment: integration
timeout-minutes: 60
env:
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDINGS_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
defaults:
run:
@@ -81,11 +82,19 @@ jobs:
- name: Test with pytest (OpenAI integration)
run: >
uv run pytest --import-mode=importlib
packages/core/tests/openai
-m integration
packages/openai/tests
-m "integration and not azure"
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-openai
path: ./python/pytest.xml
if-no-files-found: ignore
# Azure OpenAI integration tests
python-tests-azure-openai:
@@ -94,9 +103,10 @@ jobs:
environment: integration
timeout-minutes: 60
env:
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_MODEL: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
defaults:
run:
@@ -121,13 +131,23 @@ jobs:
- name: Test with pytest (Azure OpenAI integration)
run: >
uv run pytest --import-mode=importlib
packages/core/tests/azure
packages/openai/tests/openai/test_openai_chat_completion_client_azure.py
packages/openai/tests/openai/test_openai_chat_client_azure.py
packages/openai/tests/openai/test_openai_embedding_client_azure.py
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-azure-openai
path: ./python/pytest.xml
if-no-files-found: ignore
# Misc integration tests (Anthropic, Ollama, MCP)
# Misc integration tests (Anthropic, Hyperlight, Ollama, MCP)
python-tests-misc-integration:
name: Python Integration Tests - Misc
runs-on: ubuntu-latest
@@ -135,7 +155,7 @@ jobs:
timeout-minutes: 60
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_CHAT_MODEL_ID: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
ANTHROPIC_CHAT_MODEL: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
defaults:
run:
@@ -151,16 +171,52 @@ jobs:
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Test with pytest (Anthropic, Ollama, MCP integration)
- name: Start local MCP server
id: local-mcp
uses: ./.github/actions/setup-local-mcp-server
with:
fallback_url: ${{ env.LOCAL_MCP_URL }}
- name: Prefer local MCP URL when available
run: echo "LOCAL_MCP_URL=${{ steps.local-mcp.outputs.effective_url }}" >> "$GITHUB_ENV"
- name: Test with pytest (Anthropic, Hyperlight, Ollama, MCP integration)
run: >
uv run pytest --import-mode=importlib
packages/anthropic/tests
packages/hyperlight/tests
packages/ollama/tests
packages/core/tests/core/test_mcp.py
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--retries 2 --retry-delay 30
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-misc
path: ./python/pytest.xml
if-no-files-found: ignore
- name: Stop local MCP server
if: always()
shell: bash
run: |
set -euo pipefail
server_pid="${{ steps.local-mcp.outputs.pid }}"
if [[ -z "$server_pid" ]]; then
exit 0
fi
if ! kill -0 "$server_pid" 2>/dev/null; then
exit 0
fi
kill -TERM -- "-$server_pid" 2>/dev/null || kill -TERM "$server_pid" 2>/dev/null || true
for _ in $(seq 1 10); do
if ! kill -0 "$server_pid" 2>/dev/null; then
exit 0
fi
sleep 1
done
kill -KILL -- "-$server_pid" 2>/dev/null || kill -KILL "$server_pid" 2>/dev/null || true
# Azure Functions + Durable Task integration tests
python-tests-functions:
@@ -170,12 +226,17 @@ jobs:
timeout-minutes: 60
env:
UV_PYTHON: "3.11"
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FUNCTIONS_WORKER_RUNTIME: "python"
DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
AzureWebJobsStorage: "UseDevelopmentStorage=true"
@@ -209,18 +270,33 @@ jobs:
packages/durabletask/tests/integration_tests
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
-x
--timeout=360 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-functions
path: ./python/pytest.xml
if-no-files-found: ignore
# Azure AI integration tests
python-tests-azure-ai:
name: Python Integration Tests - Azure AI
# Foundry integration tests
python-tests-foundry:
name: Python Integration Tests - Foundry
runs-on: ubuntu-latest
environment: integration
timeout-minutes: 60
env:
AZURE_AI_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME }}
FOUNDRY_AGENT_VERSION: ${{ vars.FOUNDRY_AGENT_VERSION }}
FOUNDRY_MODELS_ENDPOINT: ${{ vars.FOUNDRY_MODELS_ENDPOINT || '' }}
FOUNDRY_MODELS_API_KEY: ${{ secrets.FOUNDRY_MODELS_API_KEY || '' }}
FOUNDRY_EMBEDDING_MODEL: ${{ vars.FOUNDRY_EMBEDDING_MODEL || '' }}
FOUNDRY_IMAGE_EMBEDDING_MODEL: ${{ vars.FOUNDRY_IMAGE_EMBEDDING_MODEL || '' }}
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
defaults:
run:
@@ -244,7 +320,21 @@ jobs:
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Test with pytest
timeout-minutes: 15
run: uv run --directory packages/azure-ai poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5
run: >
uv run pytest --import-mode=importlib
packages/foundry/tests
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-foundry
path: ./python/pytest.xml
if-no-files-found: ignore
# Azure Cosmos integration tests
python-tests-cosmos:
@@ -289,7 +379,80 @@ jobs:
echo "Cosmos DB emulator did not become ready in time." >&2
exit 1
- name: Test with pytest (Cosmos integration)
run: uv run --directory packages/azure-cosmos poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5
run: uv run --directory packages/azure-cosmos poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5 --junitxml=${{ github.workspace }}/python/pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-cosmos
path: ./python/pytest.xml
if-no-files-found: ignore
# Flaky test trend report (aggregates per-job JUnit XML results)
python-flaky-test-report:
name: Flaky Test Report
if: >
always() &&
(contains(join(needs.*.result, ','), 'success') ||
contains(join(needs.*.result, ','), 'failure'))
needs:
[
python-tests-openai,
python-tests-azure-openai,
python-tests-misc-integration,
python-tests-functions,
python-tests-foundry,
python-tests-cosmos,
]
runs-on: ubuntu-latest
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
- name: Set up python and install the project
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Download all test results from current run
uses: actions/download-artifact@v4
with:
pattern: test-results-*
path: test-results/
- name: Restore flaky report history cache
uses: actions/cache/restore@v4
with:
path: python/flaky-report-history.json
key: flaky-report-history-integration-${{ github.run_id }}
restore-keys: |
flaky-report-history-integration-
- name: Generate trend report
run: >
uv run python scripts/flaky_report/aggregate.py
../test-results/
flaky-report-history.json
flaky-test-report.md
- name: Post to Job Summary
if: always()
run: cat flaky-test-report.md >> $GITHUB_STEP_SUMMARY
- name: Save flaky report history cache
if: always()
uses: actions/cache/save@v4
with:
path: python/flaky-report-history.json
key: flaky-report-history-integration-${{ github.run_id }}
- name: Upload unified trend report
if: always()
uses: actions/upload-artifact@v7
with:
name: flaky-test-report
path: |
python/flaky-test-report.md
python/flaky-report-history.json
python-integration-tests-check:
if: always()
@@ -301,7 +464,7 @@ jobs:
python-tests-azure-openai,
python-tests-misc-integration,
python-tests-functions,
python-tests-azure-ai,
python-tests-foundry,
python-tests-cosmos
]
steps:
+210 -42
View File
@@ -37,7 +37,7 @@ jobs:
azureChanged: ${{ steps.filter.outputs.azure }}
miscChanged: ${{ steps.filter.outputs.misc }}
functionsChanged: ${{ steps.filter.outputs.functions }}
azureAiChanged: ${{ steps.filter.outputs.azure-ai }}
foundryChanged: ${{ steps.filter.outputs.foundry }}
cosmosChanged: ${{ steps.filter.outputs.cosmos }}
steps:
- uses: actions/checkout@v6
@@ -47,6 +47,9 @@ jobs:
filters: |
python:
- 'python/**'
- '.github/actions/setup-local-mcp-server/**'
- '.github/workflows/python-merge-tests.yml'
- '.github/workflows/python-integration-tests.yml'
core:
- 'python/packages/core/agent_framework/_*.py'
- 'python/packages/core/agent_framework/_workflows/**'
@@ -54,20 +57,29 @@ jobs:
- 'python/packages/core/agent_framework/observability.py'
openai:
- 'python/packages/core/agent_framework/openai/**'
- 'python/packages/core/tests/openai/**'
- 'python/packages/openai/**'
- 'python/samples/**/providers/openai/**'
azure:
- 'python/packages/openai/**'
- 'python/packages/core/agent_framework/azure/**'
- 'python/packages/core/tests/azure/**'
- 'python/samples/**/providers/azure/**'
misc:
- 'python/packages/anthropic/**'
- 'python/packages/hyperlight/**'
- 'python/packages/ollama/**'
- 'python/packages/core/agent_framework/_mcp.py'
- 'python/packages/core/tests/core/test_mcp.py'
- 'python/scripts/local_mcp_streamable_http_server.py'
- '.github/actions/setup-local-mcp-server/**'
- '.github/workflows/python-merge-tests.yml'
- '.github/workflows/python-integration-tests.yml'
functions:
- 'python/packages/azurefunctions/**'
- 'python/packages/durabletask/**'
azure-ai:
- 'python/packages/azure-ai/**'
foundry:
- 'python/packages/foundry/**'
- 'python/samples/**/providers/foundry/**'
- 'python/samples/02-agents/embeddings/foundry_embeddings.py'
cosmos:
- 'python/packages/azure-cosmos/**'
# run only if 'python' files were changed
@@ -104,12 +116,13 @@ jobs:
-m "not integration"
--timeout=120 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--junitxml=pytest.xml
working-directory: ./python
- name: Surface failing tests
if: always()
uses: pmeier/pytest-results-action@v0.7.2
with:
path: ./python/**.xml
path: ./python/pytest.xml
summary: true
display-options: fEX
fail-on-empty: false
@@ -128,9 +141,10 @@ jobs:
runs-on: ubuntu-latest
environment: integration
env:
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDINGS_MODEL_ID: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
defaults:
run:
@@ -146,11 +160,12 @@ jobs:
- name: Test with pytest (OpenAI integration)
run: >
uv run pytest --import-mode=importlib
packages/core/tests/openai
-m integration
packages/openai/tests
-m "integration and not azure"
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--junitxml=pytest.xml
working-directory: ./python
- name: Test OpenAI samples
timeout-minutes: 10
@@ -161,11 +176,18 @@ jobs:
if: always()
uses: pmeier/pytest-results-action@v0.7.2
with:
path: ./python/**.xml
path: ./python/pytest.xml
summary: true
display-options: fEX
fail-on-empty: false
title: OpenAI integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-openai
path: ./python/pytest.xml
if-no-files-found: ignore
# Azure OpenAI integration tests
python-tests-azure-openai:
@@ -180,9 +202,10 @@ jobs:
runs-on: ubuntu-latest
environment: integration
env:
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_MODEL: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
defaults:
run:
@@ -205,11 +228,14 @@ jobs:
- name: Test with pytest (Azure OpenAI integration)
run: >
uv run pytest --import-mode=importlib
packages/core/tests/azure
packages/openai/tests/openai/test_openai_chat_completion_client_azure.py
packages/openai/tests/openai/test_openai_chat_client_azure.py
packages/openai/tests/openai/test_openai_embedding_client_azure.py
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--junitxml=pytest.xml
working-directory: ./python
- name: Test Azure samples
timeout-minutes: 10
@@ -220,11 +246,18 @@ jobs:
if: always()
uses: pmeier/pytest-results-action@v0.7.2
with:
path: ./python/**.xml
path: ./python/pytest.xml
summary: true
display-options: fEX
fail-on-empty: false
title: Azure OpenAI integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-azure-openai
path: ./python/pytest.xml
if-no-files-found: ignore
# Misc integration tests (Anthropic, Ollama, MCP)
python-tests-misc-integration:
@@ -240,7 +273,7 @@ jobs:
environment: integration
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_CHAT_MODEL_ID: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
ANTHROPIC_CHAT_MODEL: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
defaults:
run:
@@ -253,26 +286,62 @@ jobs:
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Test with pytest (Anthropic, Ollama, MCP integration)
- name: Start local MCP server
id: local-mcp
uses: ./.github/actions/setup-local-mcp-server
with:
fallback_url: ${{ env.LOCAL_MCP_URL }}
- name: Prefer local MCP URL when available
run: echo "LOCAL_MCP_URL=${{ steps.local-mcp.outputs.effective_url }}" >> "$GITHUB_ENV"
- name: Test with pytest (Anthropic, Hyperlight, Ollama, MCP integration)
run: >
uv run pytest --import-mode=importlib
packages/anthropic/tests
packages/hyperlight/tests
packages/ollama/tests
packages/core/tests/core/test_mcp.py
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--retries 2 --retry-delay 30
--junitxml=pytest.xml
working-directory: ./python
- name: Stop local MCP server
if: always()
shell: bash
run: |
set -euo pipefail
server_pid="${{ steps.local-mcp.outputs.pid }}"
if [[ -z "$server_pid" ]]; then
exit 0
fi
if ! kill -0 "$server_pid" 2>/dev/null; then
exit 0
fi
kill -TERM -- "-$server_pid" 2>/dev/null || kill -TERM "$server_pid" 2>/dev/null || true
for _ in $(seq 1 10); do
if ! kill -0 "$server_pid" 2>/dev/null; then
exit 0
fi
sleep 1
done
kill -KILL -- "-$server_pid" 2>/dev/null || kill -KILL "$server_pid" 2>/dev/null || true
- name: Surface failing tests
if: always()
uses: pmeier/pytest-results-action@v0.7.2
with:
path: ./python/**.xml
path: ./python/pytest.xml
summary: true
display-options: fEX
fail-on-empty: false
title: Misc integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-misc
path: ./python/pytest.xml
if-no-files-found: ignore
# Azure Functions + Durable Task integration tests
python-tests-functions:
@@ -288,12 +357,17 @@ jobs:
environment: integration
env:
UV_PYTHON: "3.11"
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FUNCTIONS_WORKER_RUNTIME: "python"
DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
AzureWebJobsStorage: "UseDevelopmentStorage=true"
@@ -325,33 +399,48 @@ jobs:
packages/durabletask/tests/integration_tests
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
-x
--timeout=360 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--junitxml=pytest.xml
working-directory: ./python
- name: Surface failing tests
if: always()
uses: pmeier/pytest-results-action@v0.7.2
with:
path: ./python/**.xml
path: ./python/pytest.xml
summary: true
display-options: fEX
fail-on-empty: false
title: Functions integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-functions
path: ./python/pytest.xml
if-no-files-found: ignore
python-tests-azure-ai:
name: Python Tests - Azure AI
python-tests-foundry:
name: Python Integration Tests - Foundry
needs: paths-filter
if: >
github.event_name != 'pull_request' &&
needs.paths-filter.outputs.pythonChanges == 'true' &&
(github.event_name != 'merge_group' ||
needs.paths-filter.outputs.azureAiChanged == 'true' ||
needs.paths-filter.outputs.foundryChanged == 'true' ||
needs.paths-filter.outputs.coreChanged == 'true')
runs-on: ubuntu-latest
environment: integration
env:
AZURE_AI_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME }}
FOUNDRY_AGENT_VERSION: ${{ vars.FOUNDRY_AGENT_VERSION }}
FOUNDRY_MODELS_ENDPOINT: ${{ vars.FOUNDRY_MODELS_ENDPOINT || '' }}
FOUNDRY_MODELS_API_KEY: ${{ secrets.FOUNDRY_MODELS_API_KEY || '' }}
FOUNDRY_EMBEDDING_MODEL: ${{ vars.FOUNDRY_EMBEDDING_MODEL || '' }}
FOUNDRY_IMAGE_EMBEDDING_MODEL: ${{ vars.FOUNDRY_IMAGE_EMBEDDING_MODEL || '' }}
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
defaults:
run:
@@ -373,22 +462,31 @@ jobs:
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Test with pytest
timeout-minutes: 15
run: uv run --directory packages/azure-ai poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5
working-directory: ./python
- name: Test Azure AI samples
timeout-minutes: 10
if: env.RUN_SAMPLES_TESTS == 'true'
run: uv run pytest tests/samples/ -m "azure-ai"
run: >
uv run pytest --import-mode=importlib
packages/foundry/tests
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--junitxml=pytest.xml
working-directory: ./python
- name: Surface failing tests
if: always()
uses: pmeier/pytest-results-action@v0.7.2
with:
path: ./python/**.xml
path: ./python/pytest.xml
summary: true
display-options: fEX
fail-on-empty: false
title: Test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-foundry
path: ./python/pytest.xml
if-no-files-found: ignore
# TODO: Add python-tests-lab
@@ -438,17 +536,87 @@ jobs:
echo "Cosmos DB emulator did not become ready in time." >&2
exit 1
- name: Test with pytest (Cosmos integration)
run: uv run --directory packages/azure-cosmos poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5
run: uv run --directory packages/azure-cosmos poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5 --junitxml=${{ github.workspace }}/python/pytest.xml
working-directory: ./python
- name: Surface failing tests
if: always()
uses: pmeier/pytest-results-action@v0.7.2
with:
path: ./python/**.xml
path: ./python/pytest.xml
summary: true
display-options: fEX
fail-on-empty: false
title: Cosmos integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-cosmos
path: ./python/pytest.xml
if-no-files-found: ignore
# Flaky test trend report (aggregates per-job JUnit XML results)
python-flaky-test-report:
name: Flaky Test Report
if: >
always() &&
(contains(join(needs.*.result, ','), 'success') ||
contains(join(needs.*.result, ','), 'failure'))
needs:
[
python-tests-openai,
python-tests-azure-openai,
python-tests-misc-integration,
python-tests-functions,
python-tests-foundry,
python-tests-cosmos,
]
runs-on: ubuntu-latest
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- name: Set up python and install the project
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Download all test results from current run
uses: actions/download-artifact@v4
with:
pattern: test-results-*
path: test-results/
- name: Restore flaky report history cache
uses: actions/cache/restore@v4
with:
path: python/flaky-report-history.json
key: flaky-report-history-merge-${{ github.run_id }}
restore-keys: |
flaky-report-history-merge-
- name: Generate trend report
run: >
uv run python scripts/flaky_report/aggregate.py
../test-results/
flaky-report-history.json
flaky-test-report.md
- name: Post to Job Summary
if: always()
run: cat flaky-test-report.md >> $GITHUB_STEP_SUMMARY
- name: Save flaky report history cache
if: always()
uses: actions/cache/save@v4
with:
path: python/flaky-report-history.json
key: flaky-report-history-merge-${{ github.run_id }}
- name: Upload unified trend report
if: always()
uses: actions/upload-artifact@v7
with:
name: flaky-test-report
path: |
python/flaky-test-report.md
python/flaky-report-history.json
python-integration-tests-check:
if: always()
@@ -460,7 +628,7 @@ jobs:
python-tests-azure-openai,
python-tests-misc-integration,
python-tests-functions,
python-tests-azure-ai,
python-tests-foundry,
python-tests-cosmos,
]
steps:
+485 -55
View File
@@ -23,10 +23,8 @@ jobs:
environment: integration
env:
# Required configuration for get-started samples
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
defaults:
run:
working-directory: python
@@ -41,6 +39,11 @@ jobs:
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 01-get-started --save-report --report-name 01-get-started
@@ -50,24 +53,29 @@ jobs:
if: always()
with:
name: validation-report-01-get-started
path: python/scripts/sample_validation/reports/
path: python/samples/sample_validation/reports/
validate-02-agents:
name: Validate 02-agents
runs-on: ubuntu-latest
environment: integration
env:
# Azure AI configuration
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Foundry configuration
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_MODEL: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME || vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
# OpenAI configuration
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
# GitHub MCP
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
# Observability
ENABLE_INSTRUMENTATION: "true"
defaults:
@@ -84,29 +92,152 @@ jobs:
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
echo "AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL" >> .env
echo "AZURE_OPENAI_CHAT_COMPLETION_MODEL=$AZURE_OPENAI_CHAT_COMPLETION_MODEL" >> .env
echo "AZURE_OPENAI_CHAT_MODEL=$AZURE_OPENAI_CHAT_MODEL" >> .env
echo "AZURE_OPENAI_EMBEDDING_MODEL=$AZURE_OPENAI_EMBEDDING_MODEL" >> .env
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
echo "GITHUB_PAT=$GITHUB_PAT" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents --save-report --report-name 02-agents
cd scripts && uv run python -m sample_validation --subdir 02-agents --exclude providers --save-report --report-name 02-agents
- name: Upload validation report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-report-02-agents
path: python/scripts/sample_validation/reports/
path: python/samples/sample_validation/reports/
validate-03-workflows:
name: Validate 03-workflows
validate-02-agents-openai:
name: Validate 02-agents/providers/openai
runs-on: ubuntu-latest
environment: integration
env:
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
echo "OPENAI_MODEL=$OPENAI_MODEL" >> .env
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/openai --save-report --report-name 02-agents-openai
- name: Upload validation report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-report-02-agents-openai
path: python/samples/sample_validation/reports/
validate-02-agents-azure:
name: Validate 02-agents/providers/azure
runs-on: ubuntu-latest
environment: integration
env:
# Azure AI configuration
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_API_VERSION: ${{ vars.AZURE_OPENAI_API_VERSION || '' }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
echo "AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL" >> .env
echo "AZURE_OPENAI_API_VERSION=$AZURE_OPENAI_API_VERSION" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure --save-report --report-name 02-agents-azure
- name: Upload validation report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-report-02-agents-azure
path: python/samples/sample_validation/reports/
validate-02-agents-anthropic:
name: Validate 02-agents/providers/anthropic
runs-on: ubuntu-latest
environment: integration
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_CHAT_MODEL: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" >> .env
echo "ANTHROPIC_CHAT_MODEL=$ANTHROPIC_CHAT_MODEL" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/anthropic --save-report --report-name 02-agents-anthropic
- name: Upload validation report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-report-02-agents-anthropic
path: python/samples/sample_validation/reports/
validate-02-agents-github-copilot:
name: Validate 02-agents/providers/github_copilot
runs-on: ubuntu-latest
environment: integration
defaults:
run:
working-directory: python
@@ -121,6 +252,220 @@ jobs:
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/github_copilot --save-report --report-name 02-agents-github-copilot
- name: Upload validation report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-report-02-agents-github-copilot
path: python/samples/sample_validation/reports/
validate-02-agents-amazon:
name: Validate 02-agents/providers/amazon
if: false # Temporarily disabled - requires AWS credentials
runs-on: ubuntu-latest
environment: integration
env:
BEDROCK_CHAT_MODEL: ${{ vars.BEDROCK__CHATMODELID }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/amazon --save-report --report-name 02-agents-amazon
- name: Upload validation report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-report-02-agents-amazon
path: python/samples/sample_validation/reports/
validate-02-agents-ollama:
name: Validate 02-agents/providers/ollama
if: false # Temporarily disabled - requires local Ollama server
runs-on: ubuntu-latest
environment: integration
env:
OLLAMA_MODEL: ${{ vars.OLLAMA__MODEL }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/ollama --save-report --report-name 02-agents-ollama
- name: Upload validation report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-report-02-agents-ollama
path: python/samples/sample_validation/reports/
validate-02-agents-foundry:
name: Validate 02-agents/providers/foundry
if: false # Temporarily disabled - provider folder also contains the local Foundry sample
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME || '' }}
FOUNDRY_AGENT_VERSION: ${{ vars.FOUNDRY_AGENT_VERSION || '' }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
echo "FOUNDRY_AGENT_NAME=$FOUNDRY_AGENT_NAME" >> .env
echo "FOUNDRY_AGENT_VERSION=$FOUNDRY_AGENT_VERSION" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/foundry --save-report --report-name 02-agents-foundry
- name: Upload validation report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-report-02-agents-foundry
path: python/samples/sample_validation/reports/
validate-02-agents-copilotstudio:
name: Validate 02-agents/providers/copilotstudio
if: false # Temporarily disabled - requires Copilot Studio setup
runs-on: ubuntu-latest
environment: integration
env:
COPILOTSTUDIOAGENT__ENVIRONMENTID: ${{ secrets.COPILOTSTUDIOAGENT__ENVIRONMENTID }}
COPILOTSTUDIOAGENT__SCHEMANAME: ${{ secrets.COPILOTSTUDIOAGENT__SCHEMANAME }}
COPILOTSTUDIOAGENT__TENANTID: ${{ secrets.COPILOTSTUDIOAGENT__TENANTID }}
COPILOTSTUDIOAGENT__AGENTAPPID: ${{ secrets.COPILOTSTUDIOAGENT__AGENTAPPID }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "COPILOTSTUDIOAGENT__ENVIRONMENTID=$COPILOTSTUDIOAGENT__ENVIRONMENTID" >> .env
echo "COPILOTSTUDIOAGENT__SCHEMANAME=$COPILOTSTUDIOAGENT__SCHEMANAME" >> .env
echo "COPILOTSTUDIOAGENT__TENANTID=$COPILOTSTUDIOAGENT__TENANTID" >> .env
echo "COPILOTSTUDIOAGENT__AGENTAPPID=$COPILOTSTUDIOAGENT__AGENTAPPID" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/copilotstudio --save-report --report-name 02-agents-copilotstudio
- name: Upload validation report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-report-02-agents-copilotstudio
path: python/samples/sample_validation/reports/
validate-02-agents-custom:
name: Validate 02-agents/providers/custom
runs-on: ubuntu-latest
environment: integration
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/custom --save-report --report-name 02-agents-custom
- name: Upload validation report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-report-02-agents-custom
path: python/samples/sample_validation/reports/
validate-03-workflows:
name: Validate 03-workflows
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 03-workflows --save-report --report-name 03-workflows
@@ -130,20 +475,16 @@ jobs:
if: always()
with:
name: validation-report-03-workflows
path: python/scripts/sample_validation/reports/
path: python/samples/sample_validation/reports/
validate-04-hosting:
name: Validate 04-hosting
if: false # Temporarily disabled because of sample complexity
if: false # Temporarily disabled because of sample complexity
runs-on: ubuntu-latest
environment: integration
env:
# Azure AI configuration
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# A2A configuration
A2A_AGENT_HOST: http://localhost:5001/
defaults:
@@ -169,27 +510,26 @@ jobs:
if: always()
with:
name: validation-report-04-hosting
path: python/scripts/sample_validation/reports/
path: python/samples/sample_validation/reports/
validate-05-end-to-end:
name: Validate 05-end-to-end
if: false # Temporarily disabled because of sample complexity
if: false # Temporarily disabled because of sample complexity
runs-on: ubuntu-latest
environment: integration
env:
# Azure AI configuration
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure AI Search (for evaluation samples)
AZURE_SEARCH_ENDPOINT: ${{ secrets.AZURE_SEARCH_ENDPOINT }}
AZURE_SEARCH_API_KEY: ${{ secrets.AZURE_SEARCH_API_KEY }}
AZURE_SEARCH_INDEX_NAME: ${{ secrets.AZURE_SEARCH_INDEX_NAME }}
# Evaluation sample
AZURE_AI_MODEL_DEPLOYMENT_NAME_WORKFLOW: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_MODEL_WORKFLOW: ${{ vars.FOUNDRY_MODEL_WORKFLOW || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_MODEL_EVAL: ${{ vars.FOUNDRY_MODEL_EVAL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
defaults:
run:
working-directory: python
@@ -213,23 +553,23 @@ jobs:
if: always()
with:
name: validation-report-05-end-to-end
path: python/scripts/sample_validation/reports/
path: python/samples/sample_validation/reports/
validate-autogen-migration:
name: Validate autogen-migration
runs-on: ubuntu-latest
environment: integration
env:
# Azure AI configuration
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# OpenAI configuration
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
defaults:
run:
working-directory: python
@@ -244,6 +584,16 @@ jobs:
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
echo "AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL" >> .env
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir autogen-migration --save-report --report-name autogen-migration
@@ -253,24 +603,27 @@ jobs:
if: always()
with:
name: validation-report-autogen-migration
path: python/scripts/sample_validation/reports/
path: python/samples/sample_validation/reports/
validate-semantic-kernel-migration:
name: Validate semantic-kernel-migration
runs-on: ubuntu-latest
environment: integration
env:
# Azure AI configuration
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration for AF
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# OpenAI configuration
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration for SK
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
# OpenAI key
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
# OpenAI configuration for SK
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
# Copilot Studio
COPILOTSTUDIOAGENT__ENVIRONMENTID: ${{ secrets.COPILOTSTUDIOAGENT__ENVIRONMENTID }}
COPILOTSTUDIOAGENT__SCHEMANAME: ${{ secrets.COPILOTSTUDIOAGENT__SCHEMANAME }}
@@ -290,6 +643,20 @@ jobs:
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
echo "AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL" >> .env
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
echo "COPILOTSTUDIOAGENT__ENVIRONMENTID=$COPILOTSTUDIOAGENT__ENVIRONMENTID" >> .env
echo "COPILOTSTUDIOAGENT__SCHEMANAME=$COPILOTSTUDIOAGENT__SCHEMANAME" >> .env
echo "COPILOTSTUDIOAGENT__TENANTID=$COPILOTSTUDIOAGENT__TENANTID" >> .env
echo "COPILOTSTUDIOAGENT__AGENTAPPID=$COPILOTSTUDIOAGENT__AGENTAPPID" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir semantic-kernel-migration --save-report --report-name semantic-kernel-migration
@@ -299,4 +666,67 @@ jobs:
if: always()
with:
name: validation-report-semantic-kernel-migration
path: python/scripts/sample_validation/reports/
path: python/samples/sample_validation/reports/
aggregate-results:
name: Aggregate Results
runs-on: ubuntu-latest
if: always()
needs:
- validate-01-get-started
- validate-02-agents
- validate-02-agents-openai
- validate-02-agents-azure
- validate-02-agents-anthropic
- validate-02-agents-github-copilot
- validate-02-agents-amazon
- validate-02-agents-ollama
- validate-02-agents-foundry
- validate-02-agents-copilotstudio
- validate-02-agents-custom
- validate-03-workflows
- validate-04-hosting
- validate-05-end-to-end
- validate-autogen-migration
- validate-semantic-kernel-migration
steps:
- uses: actions/checkout@v6
- name: Download all validation reports
uses: actions/download-artifact@v7
with:
pattern: validation-report-*
path: reports/
merge-multiple: true
- name: Restore validation history
id: cache-restore
uses: actions/cache/restore@v4
with:
path: validation-history/
key: validation-history-${{ github.run_id }}
restore-keys: |
validation-history-
- name: Aggregate results and generate trend report
run: |
python3 python/scripts/sample_validation/aggregate.py \
reports/ \
validation-history/history.json \
trend-report.md
- name: Write trend report to job summary
run: cat trend-report.md >> "$GITHUB_STEP_SUMMARY"
- name: Save validation history
uses: actions/cache/save@v4
with:
path: validation-history/
key: validation-history-${{ github.run_id }}
- name: Upload trend report
uses: actions/upload-artifact@v7
if: always()
with:
name: validation-trend-report
path: trend-report.md
@@ -21,7 +21,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Download coverage report
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
run-id: ${{ github.event.workflow_run.id }}
+2 -2
View File
@@ -40,7 +40,7 @@ jobs:
UV_CACHE_DIR: /tmp/.uv-cache
# Unit tests
- name: Run all tests
run: uv run poe test -A
run: uv run poe test -A --junitxml=pytest.xml
working-directory: ./python
# Surface failing tests
@@ -48,7 +48,7 @@ jobs:
if: always()
uses: pmeier/pytest-results-action@v0.7.2
with:
path: ./python/**.xml
path: ./python/pytest.xml
summary: true
display-options: fEX
fail-on-empty: false
+12
View File
@@ -47,6 +47,8 @@ htmlcov/
.cache
nosetests.xml
coverage.xml
pytest.xml
python-coverage.xml
*.cover
*.py,cover
.hypothesis/
@@ -134,6 +136,10 @@ celerybeat.pid
.venv
env/
venv/
# Foundry agent CLI (contains secrets, auto-generated)
.foundry-agent.json
.foundry-agent-build.log
ENV/
env.bak/
venv.bak/
@@ -201,6 +207,8 @@ temp*/
# AI
.claude/
.omc/
.omx/
WARP.md
**/memory-bank/
**/projectBrief.md
@@ -230,3 +238,7 @@ local.settings.json
# Database files
*.db
python/dotnet-ref
# Generated filtered solution files (created by eng/scripts/New-FilteredSolution.ps1)
dotnet/filtered-*.slnx
**/*.lscache
+47 -8
View File
@@ -74,6 +74,37 @@ Contributions must maintain API signature and behavioral compatibility. Contribu
that include breaking changes will be rejected. Please file an issue to discuss
your idea or change if you believe that a breaking change is warranted.
#### Automated API Compatibility Validation
The .NET projects use [Package Validation](https://learn.microsoft.com/dotnet/fundamentals/package-validation/overview)
to automatically detect API breaking changes. This validation runs during `dotnet build`
(Release configuration) and `dotnet pack`, comparing the current API surface against the
latest published NuGet baseline version.
**What gets validated:** By default, packable RC packages (`IsReleaseCandidate=true`) and
GA packages (`IsGenerallyAvailable=true`) that have a published NuGet baseline and do not
override validation settings are automatically validated. The shared baseline version and
default validation settings are defined in `dotnet/nuget/nuget-package.props`, but
individual projects may opt out (for example by setting `EnablePackageValidation=false`).
**If the build fails with CP errors (e.g., CP0001, CP0002):**
1. **Unintentional breaking change** — Refactor your code to maintain backward compatibility.
2. **Intentional breaking change** (approved by maintainers) — Generate a suppression file:
```bash
dotnet build <project>.csproj -c Release /p:ApiCompatGenerateSuppressionFile=true
```
This creates or updates a `CompatibilitySuppressions.xml` in the project directory.
Include this file in your PR with justification for the breaking change.
**After each release:**
1. Delete all `CompatibilitySuppressions.xml` files from validated projects.
2. Update `PackageValidationBaselineVersion` in `dotnet/nuget/nuget-package.props` to the
newly published version.
For more details, see the [Package Validation diagnostic IDs](https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids).
### Suggested Workflow
We use and recommend the following workflow:
@@ -92,22 +123,30 @@ We use and recommend the following workflow:
"issue-123" or "githubhandle-issue".
4. Make and commit your changes to your branch.
5. Add new tests corresponding to your change, if applicable.
6. Run the relevant scripts in [the section below](#development-scripts) to ensure that your build is clean and all tests are passing.
6. Run the relevant scripts in [the section below](#development-setup) to ensure that your build is clean and all tests are passing.
7. Create a PR against the repository's **main** branch.
- State in the description what issue or improvement your change is addressing.
- Verify that all the Continuous Integration checks are passing.
8. Wait for feedback or approval of your changes from the code maintainers.
9. When area owners have signed off, and all checks are green, your PR will be merged.
### Development scripts
### Development Setup
The scripts below are used to build, test, and lint within the project.
Each language has its own dev setup guide, coding standards, and build scripts:
- Python: see [python/DEV_SETUP.md](./python/DEV_SETUP.md).
- .NET:
- Build: `dotnet build`
- Test: `dotnet test`
- Linting (auto-fix): `dotnet format`
- **Python**: [Dev Setup](./python/DEV_SETUP.md) · [Coding Standard](./python/CODING_STANDARD.md) · [README](./python/README.md)
- From the `./python` directory:
- Build: `uv run poe build`
- Unit tests: `uv run poe test -A -m "not integration"`
- Integration tests: `uv run poe test -A -m integration` (requires API keys/endpoints)
- Format + lint: `uv run poe syntax`
- All checks: `uv run poe check`
- **.NET**: [README](./dotnet/README.md) · [Agent Instructions](./dotnet/AGENTS.md)
- From the `./dotnet` directory:
- Build: `dotnet build`
- Unit tests: `dotnet test --filter-query "/*UnitTests*/*/*/*"`
- Integration tests: `dotnet test --filter-query "/*IntegrationTests*/*/*/*"` (requires API keys/endpoints)
- Linting (auto-fix): `dotnet format`
### PR - CI Process
+77 -47
View File
@@ -2,7 +2,7 @@
# Welcome to Microsoft Agent Framework!
[![Microsoft Azure AI Foundry Discord](https://dcbadge.limes.pink/api/server/b5zjErwbQM?style=flat)](https://discord.gg/b5zjErwbQM)
[![Microsoft Foundry Discord](https://dcbadge.limes.pink/api/server/b5zjErwbQM?style=flat)](https://discord.gg/b5zjErwbQM)
[![MS Learn Documentation](https://img.shields.io/badge/MS%20Learn-Documentation-blue)](https://learn.microsoft.com/en-us/agent-framework/)
[![PyPI](https://img.shields.io/pypi/v/agent-framework)](https://pypi.org/project/agent-framework/)
[![NuGet](https://img.shields.io/nuget/v/Microsoft.Agents.AI)](https://www.nuget.org/profiles/MicrosoftAgentFramework/)
@@ -28,7 +28,7 @@ Welcome to Microsoft's comprehensive multi-language framework for building, orch
Python
```bash
pip install agent-framework --pre
pip install agent-framework
# This will install all sub-packages, see `python/packages` for individual packages.
# It may take a minute on first install on Windows.
```
@@ -90,27 +90,27 @@ Still have questions? Join our [weekly office hours](./COMMUNITY.md#public-commu
Create a simple Azure Responses Agent that writes a haiku about the Microsoft Agent Framework
```python
# pip install agent-framework --pre
# pip install agent-framework
# Use `az login` to authenticate with Azure CLI
import os
import asyncio
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
async def main():
# Initialize a chat agent with Azure OpenAI Responses
# Initialize a chat agent with Microsoft Foundry
# the endpoint, deployment name, and api version can be set via environment variables
# or they can be passed in directly to the AzureOpenAIResponsesClient constructor
agent = AzureOpenAIResponsesClient(
# endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
# deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"],
# api_version=os.environ["AZURE_OPENAI_API_VERSION"],
# api_key=os.environ["AZURE_OPENAI_API_KEY"], # Optional if using AzureCliCredential
credential=AzureCliCredential(), # Optional, if using api_key
).as_agent(
name="HaikuBot",
instructions="You are an upbeat assistant that writes beautifully.",
# or they can be passed in directly to the FoundryChatClient constructor
agent = Agent(
client=FoundryChatClient(
credential=AzureCliCredential(),
# project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
# model=os.environ["FOUNDRY_MODEL_DEPLOYMENT_NAME"],
),
name="HaikuBot",
instructions="You are an upbeat assistant that writes beautifully.",
)
print(await agent.run("Write a haiku about Microsoft Agent Framework."))
@@ -120,41 +120,38 @@ if __name__ == "__main__":
```
### Basic Agent - .NET
Create a simple Agent, using Microsoft Foundry with token-based auth, that writes a haiku about the Microsoft Agent Framework
```c#
// dotnet add package Microsoft.Agents.AI.Foundry
// Use `az login` to authenticate with Azure CLI
using Azure.AI.Projects;
using Azure.Identity;
using System;
using Azure.AI.Projects;
using Azure.Identity;
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(model: deploymentName, name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
```
Create a simple Agent, using OpenAI Responses, that writes a haiku about the Microsoft Agent Framework
```c#
// dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
using Microsoft.Agents.AI;
// dotnet add package Microsoft.Agents.AI.OpenAI
using System;
using OpenAI;
using OpenAI.Responses;
// Replace the <apikey> with your OpenAI API key.
var agent = new OpenAIClient("<apikey>")
.GetResponsesClient("gpt-4o-mini")
.AsAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
```
Create a simple Agent, using Azure OpenAI Responses with token based auth, that writes a haiku about the Microsoft Agent Framework
```c#
// dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
// dotnet add package Azure.Identity
// Use `az login` to authenticate with Azure CLI
using System.ClientModel.Primitives;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI;
using OpenAI.Responses;
// Replace <resource> and gpt-4o-mini with your Azure OpenAI resource name and deployment name.
var agent = new OpenAIClient(
new BearerTokenPolicy(new AzureCliCredential(), "https://ai.azure.com/.default"),
new OpenAIClientOptions() { Endpoint = new Uri("https://<resource>.openai.azure.com/openai/v1") })
.GetResponsesClient("gpt-4o-mini")
.AsAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
.GetResponsesClient()
.AsAIAgent(model: "gpt-5.4-mini", name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
```
@@ -163,15 +160,43 @@ Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Fram
### Python
- [Getting Started with Agents](./python/samples/01-get-started): progressive tutorial from hello-world to hosting
- [Getting Started](./python/samples/01-get-started): progressive tutorial from hello-world to hosting
- [Agent Concepts](./python/samples/02-agents): deep-dive samples by topic (tools, middleware, providers, etc.)
- [Getting Started with Workflows](./python/samples/03-workflows): workflow creation and integration with agents
- [Workflows](./python/samples/03-workflows): workflow creation and integration with agents
- [Hosting](./python/samples/04-hosting): A2A, Azure Functions, Durable Task hosting
- [End-to-End](./python/samples/05-end-to-end): full applications, evaluation, and demos
### .NET
- [Getting Started with Agents](./dotnet/samples/02-agents/Agents): basic agent creation and tool usage
- [Agent Provider Samples](./dotnet/samples/02-agents/AgentProviders): samples showing different agent providers
- [Workflow Samples](./dotnet/samples/03-workflows): advanced multi-agent patterns and workflow orchestration
- [Getting Started](./dotnet/samples/01-get-started): progressive tutorial from hello agent to hosting
- [Agent Concepts](./dotnet/samples/02-agents/Agents): basic agent creation and tool usage
- [Agent Providers](./dotnet/samples/02-agents/AgentProviders): samples showing different agent providers
- [Workflows](./dotnet/samples/03-workflows): advanced multi-agent patterns and workflow orchestration
- [Hosting](./dotnet/samples/04-hosting): A2A, Durable Agents, Durable Workflows
- [End-to-End](./dotnet/samples/05-end-to-end): full applications and demos
## Troubleshooting
### Authentication
| Problem | Cause | Fix |
|---------|-------|-----|
| Authentication errors when using Azure credentials | Not signed in to Azure CLI | Run `az login` before starting your app |
| API key errors | Wrong or missing API key | Verify the key and ensure it's for the correct resource/provider |
> **Tip:** `DefaultAzureCredential` is convenient for development but in production, consider using a specific credential (e.g., `ManagedIdentityCredential`) to avoid latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
### Environment Variables
The samples typically read configuration from environment variables. Common required variables:
| Variable | Used by | Purpose |
|----------|---------|---------|
| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI samples | Your Azure OpenAI resource URL |
| `AZURE_OPENAI_DEPLOYMENT_NAME` | Azure OpenAI samples | Model deployment name (e.g. `gpt-4o-mini`) |
| `AZURE_AI_PROJECT_ENDPOINT` | Microsoft Foundry samples | Your Microsoft Foundry project endpoint |
| `AZURE_AI_MODEL_DEPLOYMENT_NAME` | Microsoft Foundry samples | Model deployment name |
| `OPENAI_API_KEY` | OpenAI (non-Azure) samples | Your OpenAI platform API key |
## Contributor Resources
@@ -182,4 +207,9 @@ Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Fram
## Important Notes
If you use the Microsoft Agent Framework to build applications that operate with third-party servers or agents, you do so at your own risk. We recommend reviewing all data being shared with third-party servers or agents and being cognizant of third-party practices for retention and location of data. It is your responsibility to manage whether your data will flow outside of your organization's Azure compliance and geographic boundaries and any related implications.
> [!IMPORTANT]
> If you use Microsoft Agent Framework to build applications that operate with any third-party servers, agents, code, or non-Azure Direct models (“Third-Party Systems”), you do so at your own risk. Third-Party Systems are Non-Microsoft Products under the Microsoft Product Terms and are governed by their own third-party license terms. You are responsible for any usage and associated costs.
>
>We recommend reviewing all data being shared with and received from Third-Party Systems and being cognizant of third-party practices for handling, sharing, retention and location of data. It is your responsibility to manage whether your data will flow outside of your organizations Azure compliance and geographic boundaries and any related implications, and that appropriate permissions, boundaries and approvals are provisioned.
>
>You are responsible for carefully reviewing and testing applications you build using Microsoft Agent Framework in the context of your specific use cases, and making all appropriate decisions and customizations. This includes implementing your own responsible AI mitigations such as metaprompt, content filters, or other safety systems, and ensuring your applications meet appropriate quality, reliability, security, and trustworthiness standards. See also: [Transparency FAQ](./TRANSPARENCY_FAQ.md)
@@ -1,3 +1,3 @@
# Declarative Agents
This folder contains sample agent definitions that can be run using the declarative agent support, for python see the [declarative agent python sample folder](../python/samples/02-agents/declarative/).
This folder contains sample agent definitions that can be run using the declarative agent support, for python see the [declarative agent python sample folder](../../python/samples/02-agents/declarative/).
@@ -3,13 +3,13 @@ name: MicrosoftLearnAgent
description: Microsoft Learn Agent
instructions: You answer questions by searching the Microsoft Learn content only.
model:
id: =Env.AZURE_FOUNDRY_PROJECT_MODEL_ID
id: =Env.FOUNDRY_MODEL
options:
temperature: 0.9
topP: 0.95
connection:
kind: remote
endpoint: =Env.AZURE_FOUNDRY_PROJECT_ENDPOINT
endpoint: =Env.FOUNDRY_PROJECT_ENDPOINT
tools:
- kind: mcp
name: microsoft_learn
@@ -10,8 +10,8 @@ Workflow workflow = DeclarativeWorkflowBuilder.Build("Marketing.yaml", options);
```
These example workflows may be executed by the workflow
[Samples](../dotnet/samples/03-workflows/Declarative)
[Samples](../../dotnet/samples/03-workflows/Declarative)
that are present in this repository.
> See the [README.md](../dotnet/samples/03-workflows/Declarative/README.md)
> See the [README.md](../../dotnet/samples/03-workflows/Declarative/README.md)
associated with the samples for configuration details.
@@ -0,0 +1,125 @@
---
status: accepted
contact: rogerbarreto
date: 2026-03-06
deciders: rogerbarreto, alliscode
consulted: ""
informed: ""
---
# Foundry agent surface stays centered on `ChatClientAgent`
## Context
The Microsoft Foundry integration exposes two distinct usage patterns:
1. Direct Responses usage, where callers provide model, instructions, and tools at runtime.
2. Server-side versioned agents, where callers create and manage `AgentVersion` resources through `AIProjectClient.Agents`.
We briefly explored adding public wrapper types such as `FoundryAgent`, `FoundryVersionedAgent`, and `FoundryResponsesChatClient` to make those paths feel more specialized. That direction created extra public types, duplicated existing `ChatClientAgent` behavior, and pushed samples toward compatibility helpers instead of the native Azure SDK flow.
## Decision
Keep the public surface centered on `ChatClientAgent`.
- Direct Responses scenarios use `AIProjectClient.AsAIAgent(...)`.
- Server-side versioned scenarios use native `AIProjectClient.Agents` APIs to create or retrieve agent resources, then wrap `AgentRecord` or `AgentVersion` with `AIProjectClient.AsAIAgent(...)`.
- Compatibility helpers such as `AIProjectClient.CreateAIAgentAsync(...)` and `AIProjectClient.GetAIAgentAsync(...)` remain only as obsolete migration shims.
- Public wrapper types `FoundryAgent`, `FoundryVersionedAgent`, `FoundryResponsesChatClient`, and `FoundryResponsesChatClientAgent` are not part of the chosen direction.
## Why
- `ChatClientAgent` is already the framework abstraction used everywhere else.
- `AIProjectClient` is the native Azure SDK entry point for versioned agent lifecycle operations.
- A single agent abstraction avoids parallel type hierarchies for the same backend.
- Samples become clearer when they show either:
- direct Responses construction via `AIProjectClient.AsAIAgent(...)`, or
- native Foundry resource management via `AIProjectClient.Agents`.
## Consequences
### Direct Responses path
Use the convenience overloads on `AIProjectClient`:
```csharp
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
ChatClientAgent agent = aiProjectClient.AsAIAgent(
model: deploymentName,
instructions: "You are good at telling jokes.",
name: "JokerAgent");
```
Or use composed `ChatClientAgent`
```csharp
ProjectResponsesClient projectResponsesClient = new(new Uri(endpoint), new DefaultAzureCredential(), new AgentReference($"model:{deploymentName}"));
ChatClientAgent agent = new(
chatClient: projectResponsesClient.AsIChatClient(),
instructions: "You are good at telling jokes.",
name: "JokerAgent");
```
This path is code-first and does not create a persistent server-side agent.
### Versioned agent path
Use the convenience overloads on `AIProjectClient`:
```csharp
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
AgentVersion version = await aiProjectClient.Agents.CreateAgentVersionAsync(
"JokerAgent",
new AgentVersionCreationOptions(
new PromptAgentDefinition(deploymentName)
{
Instructions = "You are good at telling jokes."
}));
ChatClientAgent agent = aiProjectClient.AsAIAgent(version);
```
Or use composed `ChatClientAgent`
```csharp
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
AgentVersion version = await aiProjectClient.Agents.CreateAgentVersionAsync(
"JokerAgent",
new AgentVersionCreationOptions(
new PromptAgentDefinition(deploymentName)
{
Instructions = "You are good at telling jokes."
}));
ProjectResponsesClient projectResponsesClient = aiProjectClient
.GetProjectOpenAIClient()
.GetProjectResponsesClientForAgent(new AgentReference(version.Name, version.Version));
ChatClientAgent agent = new(
chatClient: projectResponsesClient.AsIChatClient(),
name: "JokerAgent");
```
### Samples
- `FoundryAgents/` samples show the direct Responses path with `AIProjectClient.AsAIAgent(...)`.
- `FoundryVersionedAgents/` samples should show native `AIProjectClient.Agents` create/get/delete flows plus `AsAIAgent(...)`.
### Compatibility APIs
Obsolete helper extensions remain only to ease migration of existing code. New samples and new guidance should not be written against them.
## Rejected direction
Do not introduce or preserve separate public wrapper types whose main purpose is to forward to `ChatClientAgent` while carrying Foundry-specific naming.
That approach:
- duplicates lifecycle concepts already present on `AIProjectClient`,
- fragments the public API,
- complicates samples and docs,
- and makes migration harder by encouraging wrapper-specific affordances.
+960
View File
@@ -0,0 +1,960 @@
status: proposed
date: 2026-03-23
contact: sergeymenshykh
deciders: rbarreto, westey-m, eavanvalkenburg
---
# Agent Skills: Multi-Source Architecture
## Context and Problem Statement
The Agent Framework needs a skills system that lets agents discover and use domain-specific knowledge, reference documents, and executable scripts. Skills can originate from different sources — filesystem directories (SKILL.md files), inline C# code, or reusable class libraries — and the framework must support all three uniformly while allowing extensibility, composition, and filtering.
## Decision Drivers
- Skills must be definable from multiple sources: filesystem, inline code, reusable classes, etc
- Common abstractions are needed so the provider and builder work uniformly regardless of skill origin
- File-based scripts must support user-defined executors, enabling custom runtimes and languages; code/class-based scripts execute in-process as C# delegates
- Skills must be filterable so consumers can include or exclude specific skills based on defined criteria
- Multiple skill sources must be composable into a single provider
- It must be possible to add custom skill sources (e.g., databases, REST APIs, package registries) by implementing a common abstraction
## Architecture
### Model-Facing Tools
Skills are presented to the model as up to three tools that progressively disclose skill content. The system prompt lists available skill names and descriptions; the model then calls these tools on demand:
- **`load_skill(skillName)`** — returns the full skill body (instructions, listed resources, listed scripts)
- **`read_skill_resource(skillName, resourceName)`** — reads a supplementary resource (file-based or code-defined) associated with a skill
- **`run_skill_script(skillName, scriptName, arguments?)`** — executes a script associated with a skill; only registered when at least one skill contains scripts
Each tool delegates to the corresponding method on the resolved `AgentSkill` — calling `Resource.ReadAsync()` or `Script.RunAsync()` respectively.
If skills have no scripts defined, the `run_skill_script` tool is **not advertised** to the model and instructions related to script execution are **not included** in the default skills instructions.
### Abstract Base Types
The architecture defines four abstract base types that all skill variants implement:
```csharp
public abstract class AgentSkill
{
public abstract AgentSkillFrontmatter Frontmatter { get; }
public abstract string Content { get; }
public abstract IReadOnlyList<AgentSkillResource>? Resources { get; }
public abstract IReadOnlyList<AgentSkillScript>? Scripts { get; }
}
public abstract class AgentSkillResource
{
public string Name { get; }
public string? Description { get; }
public abstract Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default);
}
public abstract class AgentSkillScript
{
public string Name { get; }
public string? Description { get; }
public abstract Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default);
}
public abstract class AgentSkillsSource
{
public abstract Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default);
}
```
Skill metadata is captured via `AgentSkillFrontmatter`:
```csharp
public sealed class AgentSkillFrontmatter
{
public AgentSkillFrontmatter(string name, string description) { ... }
public string Name { get; }
public string Description { get; }
public string? License { get; set; }
public string? Compatibility { get; set; }
public string? AllowedTools { get; set; }
public AdditionalPropertiesDictionary? Metadata { get; set; }
}
```
The type hierarchy at a glance:
```
AgentSkill (abstract) AgentSkillsSource (abstract)
├── AgentFileSkill ├── AgentFileSkillsSource (public)
└── [Programmatic] ├── AgentInMemorySkillsSource (public)
├── AgentInlineSkill ├── AggregatingAgentSkillsSource (public)
└── AgentClassSkill (abstract) └── DelegatingAgentSkillsSource (abstract, public)
├── FilteringAgentSkillsSource (public)
AgentSkillResource (abstract) ├── CachingAgentSkillsSource (public)
├── AgentFileSkillResource └── DeduplicatingAgentSkillsSource (public)
└── AgentInlineSkillResource
AgentSkillScript (abstract)
├── AgentFileSkillScript
└── AgentInlineSkillScript
```
There are two top-level categories of skills:
1. **File-Based Skills** — discovered from `SKILL.md` files on the filesystem. Resources and scripts are files in subdirectories.
2. **Programmatic Skills** — defined in C# code. These are further divided into:
- **Inline Skills** — built at runtime via the `AgentInlineSkill` class and its fluent API. Ideal for quick, agent-specific skill definitions.
- **Class-Based Skills** — defined as reusable C# classes that subclass `AgentClassSkill`. Ideal for packaging skills as shared libraries or NuGet packages.
Both programmatic skill types use `AgentInlineSkillResource` and `AgentInlineSkillScript` for their resources and scripts. They are typically served by `AgentInMemorySkillsSource`, which accepts any `AgentSkill` and is not limited to programmatic skills.
### File-Based Skills
File-based skills are authored as `SKILL.md` files on disk. Resources and scripts are discovered from corresponding subfolders within the skill directory.
**`AgentFileSkill`** — A filesystem-based skill discovered from a directory containing a `SKILL.md` file. Parsed from YAML frontmatter; content is the raw markdown body. Resources and scripts are discovered from files in corresponding subfolders:
```csharp
public sealed class AgentFileSkill : AgentSkill
{
internal AgentFileSkill(
AgentSkillFrontmatter frontmatter, string content, string path,
IReadOnlyList<AgentSkillResource>? resources = null,
IReadOnlyList<AgentSkillScript>? scripts = null) { ... }
}
```
**`AgentFileSkillResource`** — A file-based skill resource. Reads content from a file on disk relative to the skill directory:
```csharp
internal sealed class AgentFileSkillResource : AgentSkillResource
{
public AgentFileSkillResource(string name, string fullPath) { ... }
public string FullPath { get; }
public override Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
{
return File.ReadAllTextAsync(FullPath, Encoding.UTF8, cancellationToken);
}
}
```
**`AgentFileSkillScript`** — A file-based skill script that represents a script file on disk. Delegates execution to an external `AgentFileSkillScriptRunner` callback (e.g., runs Python/shell via `Process.Start`). Throws `NotSupportedException` if no executor is configured:
```csharp
public delegate Task<object?> AgentFileSkillScriptRunner(
AgentFileSkill skill, AgentFileSkillScript script,
AIFunctionArguments arguments, CancellationToken cancellationToken);
public sealed class AgentFileSkillScript : AgentSkillScript
{
private readonly AgentFileSkillScriptRunner _executor;
internal AgentFileSkillScript(string name, string fullPath, AgentFileSkillScriptRunner executor)
: base(name) { ... }
public override async Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, ...)
{
return await _executor(fileSkill, this, arguments, cancellationToken);
}
}
```
The executor can be provided at the **provider level** via `AgentSkillsProviderBuilder.UseFileScriptRunner(executor)` and optionally overridden for a **particular file skill** or for a **set of skills** at the file skill source level, giving fine-grained control over how different scripts are executed.
**`AgentFileSkillsSource`** — A skill source that discovers skills from filesystem directories containing `SKILL.md` files. Recursively scans directories (max 2 levels), validates frontmatter, and enforces path traversal and symlink security checks:
```csharp
public sealed partial class AgentFileSkillsSource : AgentSkillsSource
{
public AgentFileSkillsSource(
IEnumerable<string> skillPaths,
AgentFileSkillScriptRunner scriptRunner,
AgentFileSkillsSourceOptions? options = null,
ILoggerFactory? loggerFactory = null) { ... }
}
```
**`AgentFileSkillsSourceOptions`** — Configuration options for `AgentFileSkillsSource`. Allows customizing the allowed file extensions for resources and scripts without adding constructor parameters:
```csharp
public sealed class AgentFileSkillsSourceOptions
{
public IEnumerable<string>? AllowedResourceExtensions { get; set; }
public IEnumerable<string>? AllowedScriptExtensions { get; set; }
}
```
**Example** — A file-based skill on disk and how it is added to a source:
```
skills/
└── unit-converter/
├── SKILL.md # frontmatter + instructions
├── resources/
│ └── conversion-table.csv # discovered as a resource
└── scripts/
└── convert.py # discovered as a script
```
```csharp
var source = new AgentFileSkillsSource(skillPaths: ["./skills"], scriptRunner: SubprocessScriptRunner.RunAsync);
var provider = new AgentSkillsProvider(source);
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
AIContextProviders = [provider],
});
```
### Programmatic Skills
Programmatic skills are defined in C# code rather than discovered from the filesystem. There are two kinds: **inline** and **class-based**. Both use `AgentInlineSkillResource` and `AgentInlineSkillScript` for resources and scripts, and are held by a single `AgentInMemorySkillsSource`.
**`AgentInMemorySkillsSource`** — A general-purpose skill source that holds any `AgentSkill` instances in memory. Although commonly used for programmatic skills (`AgentInlineSkill` and `AgentClassSkill`), it accepts any `AgentSkill` subclass and is not restricted to code-defined skills:
```csharp
public sealed class AgentInMemorySkillsSource : AgentSkillsSource
{
public AgentInMemorySkillsSource(
IEnumerable<AgentSkill> skills,
ILoggerFactory? loggerFactory = null) { ... }
}
```
#### Inline Skills
Inline skills are built at runtime via the `AgentInlineSkill` class and its fluent API. They are ideal for quick, agent-specific skill definitions where a full class hierarchy would be overkill.
**`AgentInlineSkill`** — A skill defined entirely in code. Resources can be static values or functions; scripts are always functions. Constructed with name, description, and instructions, then extended with resources and scripts:
```csharp
public sealed class AgentInlineSkill : AgentSkill
{
public AgentInlineSkill(string name, string description, string instructions, string? license = null, string? compatibility = null, ...) { ... }
public AgentInlineSkill(AgentSkillFrontmatter frontmatter, string instructions) { ... }
public AgentInlineSkill AddResource(object value, string name, string? description = null);
public AgentInlineSkill AddResource(Delegate handler, string name, string? description = null);
public AgentInlineSkill AddScript(Delegate handler, string name, string? description = null);
}
```
**`AgentInlineSkillResource`** — A skill resource that wraps a static value:
```csharp
public sealed class AgentInlineSkillResource : AgentSkillResource
{
public AgentInlineSkillResource(object value, string name, string? description = null)
: base(name, description)
{
_value = value;
}
public override Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
{
return Task.FromResult<object?>(_value);
}
}
```
**`AgentInlineSkillResource`** — A skill resource backed by a delegate. The delegate is invoked via an `AIFunction` each time `ReadAsync` is called, producing a dynamic (computed) value:
```csharp
public sealed class AgentInlineSkillResource : AgentSkillResource
{
public AgentInlineSkillResource(Delegate handler, string name, string? description = null)
: base(name, description)
{
_function = AIFunctionFactory.Create(handler, name: name);
}
public override async Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
{
return await _function.InvokeAsync(new AIFunctionArguments() { Services = serviceProvider }, cancellationToken);
}
}
```
**`AgentInlineSkillScript`** — A skill script backed by a delegate via an `AIFunction`:
```csharp
public sealed class AgentInlineSkillScript : AgentSkillScript
{
private readonly AIFunction _function;
public AgentInlineSkillScript(Delegate handler, string name, string? description = null)
: base(name, description)
{
_function = AIFunctionFactory.Create(handler, name: name);
}
public JsonElement? ParametersSchema => _function.JsonSchema;
public override async Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, ...)
{
return await _function.InvokeAsync(arguments, cancellationToken);
}
}
```
**Example** — Creating an inline skill with a resource and script, then adding it to a source:
```csharp
var skill = new AgentInlineSkill(
name: "unit-converter",
description: "Converts between measurement units.",
instructions: """
Use this skill to convert values between metric and imperial units.
Refer to the conversion-table resource for supported unit pairs.
Run the convert script to perform conversions.
"""
)
.AddResource("kg=2.205lb, m=3.281ft, L=0.264gal", "conversion-table", "Supported unit pairs")
.AddScript(Convert, "convert", "Converts a value between units");
var source = new AgentInMemorySkillsSource([skill]);
var provider = new AgentSkillsProvider(source);
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
AIContextProviders = [provider],
});
static string Convert(double value, double factor)
=> JsonSerializer.Serialize(new { result = Math.Round(value * factor, 4) });
```
#### Class-Based Skills
Class-based skills are designed for packaging skills as reusable libraries. Users subclass `AgentClassSkill` and override properties. Unlike inline skills, class-based skills are self-contained, can live in shared libraries or NuGet packages, and are well-suited for dependency injection.
**`AgentClassSkill`** — An abstract base class for defining skills as reusable C# classes that bundle all skill components (frontmatter, instructions, resources, scripts) together. Designed for packaging skills as distributable libraries:
```csharp
public abstract class AgentClassSkill : AgentSkill
{
public abstract string Instructions { get; }
// Content is auto-synthesized from Frontmatter + Instructions + Resources + Scripts
public override string Content =>
SkillContentBuilder.BuildContent(Frontmatter.Name, Frontmatter.Description,
SkillContentBuilder.BuildBody(Instructions, Resources, Scripts));
}
```
**Example** — Defining a class-based skill and adding it to a source:
```csharp
public class UnitConverterSkill : AgentClassSkill
{
public override AgentSkillFrontmatter Frontmatter { get; } =
new("unit-converter", "Converts between measurement units.");
public override string Instructions => """
Use this skill to convert values between metric and imperial units.
Refer to the conversion-table resource for supported unit pairs.
Run the convert script to perform conversions.
""";
public override IReadOnlyList<AgentSkillResource>? Resources { get; } =
[
new AgentInlineSkillResource("kg=2.205lb, m=3.281ft", "conversion-table"),
];
public override IReadOnlyList<AgentSkillScript>? Scripts { get; } =
[
new AgentInlineSkillScript(Convert, "convert"),
];
private static string Convert(double value, double factor)
=> JsonSerializer.Serialize(new { result = Math.Round(value * factor, 4) });
}
var source = new AgentInMemorySkillsSource([new UnitConverterSkill()]);
var provider = new AgentSkillsProvider(source);
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
AIContextProviders = [provider],
});
```
## Filtering, Caching, and Deduplication
The following subsections present alternative approaches for handling filtering, caching, and deduplication of skills across multiple sources.
### Via Composition
In this approach, the `AgentSkillsProvider` accepts a **single** `AgentSkillsSource`. Multiple sources are composed externally via an aggregate source, and cross-cutting concerns like filtering, caching, and deduplication are implemented as **source decorators** — subclasses of `DelegatingAgentSkillsSource` that intercept `GetSkillsAsync()`.
**`FilteringAgentSkillsSource`** — A decorator that applies filter logic before returning results. The decorator pattern keeps filtering orthogonal to source implementations and allows composing multiple filters:
```csharp
public sealed class FilteringAgentSkillsSource : DelegatingAgentSkillsSource
{
private readonly Func<AgentSkill, bool> _predicate;
public FilteringAgentSkillsSource(AgentSkillsSource innerSource, Func<AgentSkill, bool> predicate)
: base(innerSource)
{
_predicate = predicate;
}
public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
{
var skills = await this.InnerSource.GetSkillsAsync(cancellationToken);
return skills.Where(_predicate).ToList();
}
}
```
**`CachingAgentSkillsSource`** — A decorator that caches skills after the first load, keeping the provider stateless and giving consumers control over caching granularity per source. For example, file-based skills (expensive to discover) can be cached while code-defined skills remain uncached:
```csharp
public sealed class CachingAgentSkillsSource : DelegatingAgentSkillsSource
{
private IList<AgentSkill>? _cached;
public CachingAgentSkillsSource(AgentSkillsSource innerSource)
: base(innerSource)
{
}
public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
{
return _cached ??= await this.InnerSource.GetSkillsAsync(cancellationToken);
}
}
```
**Deduplication** is similarly implemented as a decorator (`DeduplicatingAgentSkillsSource`) that deduplicates by name (case-insensitive, first-one-wins) and logs a warning for skipped duplicates.
**Example** — Combining file-based and code-defined sources with filtering and caching:
```csharp
var fileSource = new CachingAgentSkillsSource(new AgentFileSkillsSource(["./skills"]));
var codeSource = new AgentInMemorySkillsSource([myCodeSkill]);
var compositeSource = new FilteringAgentSkillsSource(
new AggregatingAgentSkillsSource([fileSource, codeSource]),
filter: s => s.Frontmatter.Name != "internal");
var provider = new AgentSkillsProvider(compositeSource);
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
AIContextProviders = [provider],
});
```
**Pros:**
- Clean single-responsibility: the provider serves skills, sources provide them.
- Caching, filtering, and deduplication are composable as source decorators — each concern is a separate, testable wrapper.
**Cons:**
- DI is less flexible: multiple `AgentSkillsSource` implementations registered in the container cannot be auto-injected into the provider. The consumer must manually compose them via an aggregate source.
- Increased public API surface: requires additional public classes (aggregate source, caching decorators, filtering decorators) that consumers need to learn and use.
### Via AgentSkillsProvider
In this approach, the `AgentSkillsProvider` accepts **`IEnumerable<AgentSkillsSource>`** and handles aggregation, filtering, caching, and deduplication internally.
The provider aggregates skills from all registered sources, deduplicates by name (case-insensitive, first-one-wins), caches the result after the first load, and optionally applies filtering via a predicate on `AgentSkillsProviderOptions`. Duplicate skill names are logged as warnings.
**Example** — Registering multiple sources directly with the provider:
```csharp
// Conceptual example — in practice, use AgentSkillsProviderBuilder
var fileSource = new AgentFileSkillsSource(["./skills"]);
var codeSource = new AgentInMemorySkillsSource([myCodeSkill]);
var provider = new AgentSkillsProvider(
sources: [fileSource, codeSource],
options: new AgentSkillsProviderOptions
{
Filter = s => s.Frontmatter.Name != "internal",
});
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
AIContextProviders = [provider],
});
```
**Pros:**
- DI-friendly: register multiple `AgentSkillsSource` implementations in the container, and they are all auto-injected into `AgentSkillsProvider` via `IEnumerable<AgentSkillsSource>`.
- Smaller public API surface: no need for aggregate source, caching decorators, or filtering decorator classes — these concerns are handled internally by the provider.
**Cons:**
- The provider takes on multiple responsibilities — aggregation, caching, deduplication, and filtering.
- Less granular caching control: caching is all-or-nothing across sources rather than per-source as with decorators.
- Less extensible: new behaviors (e.g., ordering, TTL expiration) require modifying the provider rather than adding a decorator.
### Builder Pattern
**`AgentSkillsProviderBuilder`** provides a fluent API for composing skills from multiple sources. The builder centralizes configuration — script executors, approval callbacks, prompt templates, and filtering — so consumers don't need to know the underlying source types.
The builder internally decides how to wire up the object graph: it creates the appropriate source instances, applies caching and filtering, and returns a fully configured `AgentSkillsProvider`. This keeps the setup code concise while still allowing fine-grained control when needed.
**Example** — Using the builder to combine multiple source types with configuration:
```csharp
var provider = new AgentSkillsProviderBuilder()
.UseFileSkill("./skills") // file-based source
.UseInlineSkills(codeSkill) // code-defined source
.UseClassSkills(new ClassSkill()) // class-based source
.UseFileScriptRunner(SubprocessScriptRunner.RunAsync) // script runner
.UseScriptApproval() // optional human-in-the-loop
.UsePromptTemplate(customTemplate) // optional prompt customization
.UseFilter(s => s.Frontmatter.Name != "internal") // optional skill filtering
.Build();
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
AIContextProviders = [provider],
});
```
## Adding a Custom Skill Type
The skills framework is designed for extensibility. While file-based and inline skills cover common
scenarios, you can introduce entirely new skill types by subclassing the four base classes:
| Base class | Purpose |
|-----------------------|-----------------------------------------------------|
| `AgentSkillsSource` | Discovers and loads skills from a particular origin |
| `AgentSkill` | Holds metadata, content, resources, and scripts |
| `AgentSkillResource` | Provides supplementary content to a skill |
| `AgentSkillScript` | Represents an executable action within a skill |
The example below implements a **cloud-based skill type** where skills, resources, and scripts are
all stored in and executed through a remote cloud service (e.g., Azure Blob Storage + Azure Functions).
### Step 1 — Define a custom resource
A `CloudSkillResource` reads resource content from a cloud storage endpoint instead of the local
filesystem:
```csharp
/// <summary>
/// A skill resource backed by a cloud storage endpoint.
/// </summary>
public sealed class CloudSkillResource : AgentSkillResource
{
private readonly HttpClient _httpClient;
public CloudSkillResource(string name, Uri blobUri, HttpClient httpClient, string? description = null)
: base(name, description)
{
BlobUri = blobUri ?? throw new ArgumentNullException(nameof(blobUri));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
/// <summary>
/// Gets the URI of the cloud blob that holds this resource's content.
/// </summary>
public Uri BlobUri { get; }
/// <inheritdoc/>
public override async Task<object?> ReadAsync(
IServiceProvider? serviceProvider = null,
CancellationToken cancellationToken = default)
{
return await _httpClient.GetStringAsync(BlobUri, cancellationToken).ConfigureAwait(false);
}
}
```
### Step 2 — Define a custom script
A `CloudSkillScript` executes a script by calling a cloud function endpoint, passing arguments as
the request body:
```csharp
/// <summary>
/// A skill script executed via a cloud function endpoint.
/// </summary>
public sealed class CloudSkillScript : AgentSkillScript
{
private readonly HttpClient _httpClient;
public CloudSkillScript(string name, Uri functionUri, HttpClient httpClient, string? description = null)
: base(name, description)
{
FunctionUri = functionUri ?? throw new ArgumentNullException(nameof(functionUri));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
/// <summary>
/// Gets the URI of the cloud function that runs this script.
/// </summary>
public Uri FunctionUri { get; }
/// <inheritdoc/>
public override async Task<object?> RunAsync(
AgentSkill skill,
AIFunctionArguments arguments,
CancellationToken cancellationToken = default)
{
var json = JsonSerializer.Serialize(arguments);
using var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(FunctionUri, content, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
}
}
```
### Step 3 — Define a custom skill
A `CloudSkill` bundles cloud-specific metadata (e.g., the base endpoint) with the standard skill
shape:
```csharp
/// <summary>
/// An <see cref="AgentSkill"/> whose content, resources, and scripts are stored in a cloud service.
/// </summary>
public sealed class CloudSkill : AgentSkill
{
public CloudSkill(
AgentSkillFrontmatter frontmatter,
string content,
Uri endpoint,
IReadOnlyList<AgentSkillResource>? resources = null,
IReadOnlyList<AgentSkillScript>? scripts = null)
{
Frontmatter = frontmatter ?? throw new ArgumentNullException(nameof(frontmatter));
Content = content ?? throw new ArgumentNullException(nameof(content));
Endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint));
Resources = resources;
Scripts = scripts;
}
/// <inheritdoc/>
public override AgentSkillFrontmatter Frontmatter { get; }
/// <inheritdoc/>
public override string Content { get; }
/// <summary>
/// Gets the base cloud endpoint for this skill.
/// </summary>
public Uri Endpoint { get; }
/// <inheritdoc/>
public override IReadOnlyList<AgentSkillResource>? Resources { get; }
/// <inheritdoc/>
public override IReadOnlyList<AgentSkillScript>? Scripts { get; }
}
```
### Step 4 — Define a custom source
A `CloudSkillsSource` discovers skills from a cloud catalog API and constructs `CloudSkill`
instances with their associated resources and scripts:
```csharp
/// <summary>
/// A skill source that discovers and loads skills from a cloud catalog API.
/// </summary>
public sealed class CloudSkillsSource : AgentSkillsSource
{
private readonly Uri _catalogUri;
private readonly HttpClient _httpClient;
public CloudSkillsSource(Uri catalogUri, HttpClient httpClient)
{
_catalogUri = catalogUri ?? throw new ArgumentNullException(nameof(catalogUri));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
/// <inheritdoc/>
public override async Task<IList<AgentSkill>> GetSkillsAsync(
CancellationToken cancellationToken = default)
{
// Fetch the skill catalog from the cloud service.
var json = await _httpClient.GetStringAsync(_catalogUri, cancellationToken)
.ConfigureAwait(false);
var catalog = JsonSerializer.Deserialize<CloudSkillCatalog>(json)!;
var skills = new List<AgentSkill>();
foreach (var entry in catalog.Skills)
{
var frontmatter = new AgentSkillFrontmatter(entry.Name, entry.Description);
// Build cloud-backed resources.
var resources = entry.Resources
.Select(r => new CloudSkillResource(r.Name, r.BlobUri, _httpClient, r.Description))
.ToList<AgentSkillResource>();
// Build cloud-backed scripts.
var scripts = entry.Scripts
.Select(s => new CloudSkillScript(s.Name, s.FunctionUri, _httpClient, s.Description))
.ToList<AgentSkillScript>();
skills.Add(new CloudSkill(frontmatter, entry.Content, entry.Endpoint, resources, scripts));
}
return skills;
}
}
```
### Step 5 — Register with the builder
Use `UseSource` to wire the custom source into the provider:
```csharp
var httpClient = new HttpClient();
var provider = new AgentSkillsProviderBuilder()
.UseSource(new CloudSkillsSource(
new Uri("https://my-service.example.com/skills/catalog"),
httpClient))
// Mix with other source types if needed:
.UseFileSkill("/local/skills", scriptRunner)
.UseInlineSkills(someInlineSkill)
.Build();
```
The `AgentSkillsProvider` handles all skill types uniformly — any combination of file-based, inline,
class-based, and custom skills can coexist in the same provider. Custom skills automatically
participate in the model-facing tools (`load_skill`, `read_skill_resource`, `run_skill_script`),
filtering, deduplication, and caching — no additional integration work is required.
## Script Representation: `AgentSkillScript` vs `AIFunction`
Two approaches were considered for representing executable scripts within skills:
### Option A — Custom `AgentSkillScript` abstract base class (original design)
Scripts are modeled as a custom `AgentSkillScript` abstract class with `Name`, `Description`, and
`RunAsync(AgentSkill, AIFunctionArguments, CancellationToken)`. Concrete implementations:
`AgentInlineSkillScript` (wraps a delegate/`AIFunction`) and `AgentFileSkillScript` (wraps a file path + executor delegate).
```csharp
// Base type
public abstract class AgentSkillScript
{
public string Name { get; }
public string? Description { get; }
public abstract Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default);
}
// AgentSkill exposes scripts as:
public abstract IReadOnlyList<AgentSkillScript>? Scripts { get; }
// Inline script wraps an AIFunction internally
var script = new AgentInlineSkillScript(ConvertUnits, "convert");
// Pre-built AIFunction must be wrapped
var script = new AgentInlineSkillScript(myAIFunction);
// Class-based skill declares scripts as:
public override IReadOnlyList<AgentSkillScript>? Scripts { get; } =
[
new AgentInlineSkillScript(ConvertUnits, "convert"),
];
// Provider executes scripts by passing the owning skill:
await script.RunAsync(skill, arguments, cancellationToken);
```
**Pros:**
- **Explicit skill context at execution time.** `RunAsync` receives the owning `AgentSkill`, so any script can access skill metadata or resources during execution without requiring construction-time wiring.
- **Self-contained abstraction.** A dedicated type communicates clearly that scripts are a skills-framework concept, separate from general-purpose AI functions.
- **Easier extensibility for custom script types.** Third-party implementations can subclass `AgentSkillScript` and access the owning skill in `RunAsync` without special setup.
**Cons:**
- **Wrapper overhead.** `AgentInlineSkillScript` is a thin pass-through around `AIFunction` — it adds a class, a constructor, and an indirection layer for no behavioral difference.
- **Parallel abstraction.** `AgentSkillScript` and `AIFunction` serve overlapping purposes (named callable with arguments), creating two parallel hierarchies for the same concept.
- **Friction for consumers.** Users who already have `AIFunction` instances must wrap them in `AgentInlineSkillScript` to use them as scripts, adding ceremony.
### Option B — Reuse `AIFunction` directly
Scripts are represented as `AIFunction` (from `Microsoft.Extensions.AI`). `AgentSkill.Scripts` returns
`IReadOnlyList<AIFunction>?`. `AgentInlineSkillScript` is eliminated entirely — callers use
`AIFunctionFactory.Create(delegate, name: ...)` or pass `AIFunction` instances directly.
`AgentFileSkillScript` becomes an `AIFunction` subclass that captures its owning `AgentFileSkill` via
an internal back-reference set during construction.
```csharp
// AgentSkill exposes scripts as AIFunction directly:
public abstract IReadOnlyList<AIFunction>? Scripts { get; }
// Inline scripts use AIFunctionFactory — no wrapper class needed
var skill = new AgentInlineSkill("my-skill", "desc", "instructions");
skill.AddScript(ConvertUnits, "convert"); // delegate
skill.AddScript(myAIFunction); // pre-built AIFunction — no wrapping
// Class-based skill declares scripts as:
public override IReadOnlyList<AIFunction>? Scripts { get; } =
[
AIFunctionFactory.Create(ConvertUnits, name: "convert"),
];
// Provider executes scripts via standard AIFunction invocation:
await script.InvokeAsync(arguments, cancellationToken);
// File-based scripts extend AIFunction and capture the owning skill internally:
public sealed class AgentFileSkillScript : AIFunction
{
internal AgentFileSkill? Skill { get; set; } // set by AgentFileSkill constructor
protected override async ValueTask<object?> InvokeCoreAsync(
AIFunctionArguments arguments, CancellationToken cancellationToken)
{
return await _executor(Skill!, this, arguments, cancellationToken);
}
}
```
**Pros:**
- **Fewer types.** Eliminates `AgentSkillScript` and `AgentInlineSkillScript`, reducing the public API surface by two classes.
- **Seamless interop.** Any `AIFunction` — whether from `AIFunctionFactory`, a custom subclass, or an external library — can be used as a skill script with zero wrapping.
- **Consistent with `Microsoft.Extensions.AI` ecosystem.** Scripts share the same type as tool functions used by `IChatClient` and `FunctionInvokingChatClient`, reducing conceptual overhead for developers already familiar with the ecosystem.
**Cons:**
- **No owning-skill context in invocation signature.** `AIFunction.InvokeAsync` does not accept an `AgentSkill` parameter, so `AgentFileSkillScript` must capture its owning skill via an internal setter during construction. This adds a construction-order dependency: the skill must set the back-reference on its scripts.
- **Custom script types lose automatic skill access.** Third-party `AIFunction` subclasses that need the owning skill must implement their own mechanism (e.g., constructor injection, closure capture) instead of receiving it as a method parameter.
- **Semantic overloading.** `AIFunction` now means both "a tool the model can call" and "a script within a skill", which could blur the distinction for framework users.
## Resource Representation: `AgentSkillResource` vs `AIFunction`
Two approaches were considered for representing skill resources (supplementary content such as references, assets, or dynamic data):
### Option A — Custom `AgentSkillResource` abstract base class (original design)
Resources are modeled as a custom `AgentSkillResource` abstract class with `Name`, `Description`, and
`ReadAsync(IServiceProvider?, CancellationToken)`. Concrete implementations:
`AgentInlineSkillResource` (static value, delegate, or `AIFunction` wrapper) and `AgentFileSkillResource` (reads file content from disk).
```csharp
// Base type
public abstract class AgentSkillResource
{
public string Name { get; }
public string? Description { get; }
public abstract Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default);
}
// AgentSkill exposes resources as:
public abstract IReadOnlyList<AgentSkillResource>? Resources { get; }
// Static resource
var resource = new AgentInlineSkillResource("static content", "my-resource");
// Dynamic resource (delegate)
var resource = new AgentInlineSkillResource((IServiceProvider sp) => GetData(sp), "my-resource");
// Pre-built AIFunction must be wrapped
var resource = new AgentInlineSkillResource(myAIFunction);
// Class-based skill declares resources as:
public override IReadOnlyList<AgentSkillResource>? Resources { get; } =
[
new AgentInlineSkillResource("# Conversion Tables\n...", "conversion-table"),
];
// Provider reads resources via:
await resource.ReadAsync(serviceProvider, cancellationToken);
```
**Pros:**
- **Clear semantic distinction.** A dedicated `AgentSkillResource` type distinguishes resources (data providers) from scripts (executable actions), making the API self-documenting.
- **Purpose-built API.** `ReadAsync` communicates intent better than `InvokeAsync` for a data-access operation.
**Cons:**
- **Wrapper overhead.** `AgentInlineSkillResource` wraps `AIFunction` internally for delegate/function cases — adding a class and indirection for no behavioral difference.
- **Parallel abstraction.** `AgentSkillResource` and `AIFunction` serve overlapping purposes (named callable that returns data), creating two parallel hierarchies.
- **Friction for consumers.** Users who already have `AIFunction` instances must wrap them in `AgentInlineSkillResource`, adding ceremony.
### Option B — Reuse `AIFunction` directly
Resources are represented as `AIFunction`. `AgentSkill.Resources` returns `IReadOnlyList<AIFunction>?`.
`AgentInlineSkillResource` becomes an `AIFunction` subclass (retained as a convenience for the static-value
pattern: `new AgentInlineSkillResource("data", "name")`). `AgentFileSkillResource` becomes an `AIFunction`
subclass that reads file content.
```csharp
// AgentSkill exposes resources as AIFunction directly:
public abstract IReadOnlyList<AIFunction>? Resources { get; }
// Static resource — AgentInlineSkillResource is retained as a convenience AIFunction subclass
var resource = new AgentInlineSkillResource("static content", "my-resource");
// Dynamic resource — AgentInlineSkillResource wraps delegate as AIFunction
var resource = new AgentInlineSkillResource((IServiceProvider sp) => GetData(sp), "my-resource");
// Pre-built AIFunction can be used directly — no wrapping needed
skill.AddResource(myAIFunction);
// Class-based skill declares resources as:
public override IReadOnlyList<AIFunction>? Resources { get; } =
[
new AgentInlineSkillResource("# Conversion Tables\n...", "conversion-table"),
];
// Provider reads resources via standard AIFunction invocation:
await resource.InvokeAsync(arguments, cancellationToken);
// File-based resources extend AIFunction directly:
internal sealed class AgentFileSkillResource : AIFunction
{
public string FullPath { get; }
protected override async ValueTask<object?> InvokeCoreAsync(
AIFunctionArguments arguments, CancellationToken cancellationToken)
{
return await File.ReadAllTextAsync(FullPath, Encoding.UTF8, cancellationToken);
}
}
```
**Pros:**
- **Fewer base types.** Eliminates the `AgentSkillResource` abstract class, reducing the public API surface.
- **Seamless interop.** Any `AIFunction` can be used as a skill resource with zero wrapping.
**Cons:**
- **Loss of semantic distinction.** Resources and scripts are now both `AIFunction`, which could make it less obvious which list a function belongs to when reading code.
- **Static values require a wrapper.** Unlike the original `ReadAsync` which could return a stored value directly, `AIFunction.InvokeAsync` implies invocation. `AgentInlineSkillResource` is retained as a convenience subclass to handle the static-value case, so this is not eliminated — just moved to a different class.
## Decision Outcome
### 1. Keep `AgentSkillResource` and `AgentSkillScript` (Option A for both sections)
We are staying with the custom `AgentSkillResource` and `AgentSkillScript` model classes instead of reusing `AIFunction`:
- **Resources have no parameters.** If a consumer provides an `AIFunction` with parameters, those parameters will never be advertised to the LLM, and the resulting call will fail.
- **Approval breaks for `AIFunction`-based representations.** When a resource or script represented by an `AIFunction` is configured with approval, the second approval invocation will not work correctly.
- **Injecting the owning skill into an `AIFunction`-based script is problematic.** Constructor injection would introduce a circular reference between the skill and the script. An internal property setter is possible but adds coupling.
### 2. Make all agent skill classes internal
All agent-skill-related classes are made `internal` to minimize the public API surface while the feature matures. We can reconsider and promote types to `public` later based on community signal.
This leaves two public entry points:
- **`AgentSkillsProvider`** — use directly when all skills come from a single source and filtering is not needed.
- **`AgentSkillsProviderBuilder`** — use when mixing skill types or when filtering support is required.
### 3. Caching at provider level
Caching of tools and instructions is implemented inside `AgentSkillsProvider` rather than as an external decorator. Recreating tools and instructions on every provider call is wasteful, and a caching decorator sitting outside the provider would not have the information needed to cache them effectively.
@@ -0,0 +1,72 @@
---
status: accepted
contact: eavanvalkenburg
date: 2026-03-20
deciders: eavanvalkenburg, sphenry, chetantoshnival
consulted: taochenosu, moonbox3, dmytrostruk, giles17, alliscode
---
# Provider-Leading Client Design & OpenAI Package Extraction
## Context and Problem Statement
The `agent-framework-core` package currently bundles OpenAI and Azure OpenAI client implementations along with their dependencies (`openai`, `azure-identity`, `azure-ai-projects`, `packaging`). This makes core heavier than necessary for users who don't use OpenAI, and it conflates the core abstractions with a specific provider implementation. Additionally, the current class naming (`OpenAIResponsesClient`, `OpenAIChatClient`) is based on the underlying OpenAI API names rather than what users actually want to do, making discoverability harder for newcomers.
## Decision Drivers
- **Lightweight core**: Core should only contain abstractions, middleware infrastructure, and telemetry — no provider-specific code or dependencies.
- **Discoverability-first**: Import namespaces should guide users to the right client. `from agent_framework.openai import ...` should surface all OpenAI-related clients; `from agent_framework.azure import ...` should surface Foundry, Azure AI, and other Azure-specific classes.
- **Provider-leading naming**: The primary client name should reflect the provider, not the underlying API. The Responses API is now the recommended default for OpenAI, so its client should be called `OpenAIChatClient` (not `OpenAIResponsesClient`).
- **Clean separation of concerns**: Azure-specific deprecated wrappers belong in the azure-ai package, not in the OpenAI package.
## Considered Options
- **Keep OpenAI in core**: Simpler but keeps core heavy; doesn't help discoverability.
- **Extract OpenAI with Azure wrappers in the OpenAI package**: Keeps Azure OpenAI wrappers alongside OpenAI code, but pollutes the OpenAI package with Azure concerns.
- **Extract OpenAI, place Azure wrappers in azure-ai**: Clean separation; the OpenAI package has zero Azure dependencies; deprecated Azure wrappers live in a single file in azure-ai for easy future deletion.
## Decision Outcome
Chosen option: "Extract OpenAI, place Azure wrappers in azure-ai", because it achieves the lightest core, cleanest OpenAI package, and the most maintainable deprecation path.
Key changes:
1. **New `agent-framework-openai` package** with dependencies on `agent-framework-core`, `openai`, and `packaging` only.
2. **Class renames**: `OpenAIResponsesClient``OpenAIChatClient` (Responses API), `OpenAIChatClient``OpenAIChatCompletionClient` (Chat Completions API). Old names remain as deprecated aliases.
3. **Deprecated classes**: `OpenAIAssistantsClient`, all `AzureOpenAI*Client` classes, `AzureAIClient`, `AzureAIAgentClient`, and `AzureAIProjectAgentProvider` are marked deprecated.
4. **New `FoundryChatClient`** in azure-ai for Azure AI Foundry Responses API access, built on `RawFoundryChatClient(RawOpenAIChatClient)`.
5. **All deprecated `AzureOpenAI*` classes** consolidated into a single file (`_deprecated_azure_openai.py`) in the azure-ai package for clean future deletion.
6. **Core's `agent_framework.openai` and `agent_framework.azure` namespaces** become lazy-loading gateways, preserving backward-compatible import paths while removing hard dependencies.
7. **Unified `model` parameter** replaces `model_id` (OpenAI), `deployment_name` (Azure OpenAI), and `model_deployment_name` (Azure AI) across all client constructors. The term `model` is intentionally generic: it naturally maps to an OpenAI model name *and* to an Azure OpenAI deployment name, making it straightforward to use `OpenAIChatClient` with either OpenAI or Azure OpenAI backends (via `AsyncAzureOpenAI`). Environment variables are similarly unified (e.g., `OPENAI_MODEL` instead of separate `OPENAI_CHAT_MODEL_ID` / `OPENAI_CHAT_COMPLETION_MODEL_ID`).
8. **`FoundryAgent`** replaces the pattern of `Agent(client=AzureAIClient(...))` for connecting to pre-configured agents in Azure AI Foundry (PromptAgents and HostedAgents). The underlying `RawFoundryAgentChatClient` is an implementation detail — most users interact only with `FoundryAgent`. `AzureAIAgentClient` is separately deprecated as it refers to the V1 Agents Service API. See below for design rationale.
### Foundry Agent Design: `FoundryAgentClient` vs `FoundryAgent`
The existing `AzureAIClient` combines two concerns: CRUD lifecycle management (creating/deleting agents on the service) and runtime communication (sending messages via the Responses API). The new design removes CRUD entirely — users connect to agents that already exist in Foundry.
**Two approaches were considered:**
**Option A — `FoundryAgentClient` only (public ChatClient):**
Users compose `Agent(client=FoundryAgentClient(...), tools=[...])`. This follows the universal `Agent(client=X)` pattern used by every other provider. However, a "client" that wraps a named remote agent (with `agent_name` as a constructor param) is semantically odd — clients typically wrap a model endpoint, not a specific agent.
**Option B — `FoundryAgent` (Agent subclass) + private `_FoundryAgentChatClient` and public `RawFoundryAgentChatClient`:**
Users write `FoundryAgent(agent_name="my-agent", ...)` for the common case. Internally, `FoundryAgent` creates a `_FoundryAgentChatClient` and passes it to the standard `Agent` base class. For advanced customization, users pass `client_type=RawFoundryAgentChatClient` (or a custom subclass) to control the client middleware layers. The `Agent(client=RawFoundryAgentChatClient(...))` composition pattern still works for users who prefer it.
**Chosen option: Option B**, because:
- The common case (`FoundryAgent(...)`) is a single object with no boilerplate.
- `client_type=` gives full control over client middleware without parameter duplication — the agent forwards connection params to the client internally.
- `RawFoundryAgent(RawAgent)` and `FoundryAgent(Agent)` mirror the established `RawAgent`/`Agent` pattern.
- Runtime validation (only `FunctionTool` allowed) lives in `RawFoundryAgentChatClient._prepare_options`, ensuring it applies regardless of how the client is used — through `FoundryAgent`, `Agent(client=...)`, or any custom composition.
**Public classes:**
- `RawFoundryAgentChatClient(RawOpenAIChatClient)` — Responses API client that injects agent reference and validates tools. Extension point for custom client middleware.
- `RawFoundryAgent(RawAgent)` — Agent without agent-level middleware/telemetry.
- `FoundryAgent(AgentTelemetryLayer, AgentMiddlewareLayer, RawFoundryAgent)` — Recommended production agent.
**Internal (private):**
- `_FoundryAgentChatClient` — Full client with function invocation, chat middleware, and telemetry layers. Created automatically by `FoundryAgent`; users customize via `client_type=RawFoundryAgentChatClient` or a custom subclass.
**Deprecated:**
- `AzureAIClient` — replaced by `FoundryAgent` (which uses `FoundryAgentClient` internally).
- `AzureAIAgentClient` — refers to V1 Agents Service API, no direct replacement.
- `AzureAIProjectAgentProvider` — replaced by `FoundryAgent`.
@@ -0,0 +1,121 @@
---
status: accepted
contact: westey-m
date: 2026-03-23
deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub
consulted:
informed:
---
# Chat History Persistence Consistency
## Context and Problem Statement
When using `ChatClientAgent` with tools, the `FunctionInvokingChatClient` (FIC) loops multiple times — service call → tool execution → service call → … — before producing a final response. There are two points of discrepancy between how chat history is stored by the framework's `ChatHistoryProvider` and how the underlying AI service stores chat history (e.g., OpenAI Responses with `store=true`):
1. **Persistence timing**: The AI service persists messages after *each* service call within the FIC loop. The `ChatHistoryProvider` currently persists messages only once, at the *end* of the full agent run (after all FIC loop iterations complete).
2. **Trailing `FunctionResultContent` storage**: When tool calling is terminated mid-loop (e.g., via `FunctionInvokingChatClient` termination filters), the final response from the agent may contain `FunctionResultContent` that was never sent to a subsequent service call. The AI service never stores this trailing `FunctionResultContent`, but the `ChatHistoryProvider` currently stores all response content, including the trailing `FunctionResultContent`.
These discrepancies mean that a `ChatHistoryProvider`-managed conversation and a service-managed conversation can diverge in content and structure, even when processing the same interactions.
### Practical Impact: Resuming After Tool-Call Termination
Today, users of `AIAgent` get different behaviors depending on whether chat history is stored service-side or in a `ChatHistoryProvider`. This creates concrete challenges — for example, when the function call loop is terminated and the user wants to resume the conversation in a subsequent run. With service-stored history, the trailing `FunctionResultContent` is never persisted, so the last stored message is the `FunctionCallContent` from the service. With `ChatHistoryProvider`-stored history, the trailing `FunctionResultContent` *is* persisted. The user cannot know whether the last `FunctionResultContent` is in the chat history or not without inspecting the storage mechanism, making it difficult to write resumption logic that works correctly regardless of the storage backend.
### Relationship Between the Two Discrepancies
The persistence timing and `FunctionResultContent` trimming behaviors are interrelated:
- **Per-service-call persistence**: When messages are persisted after each individual service call, trailing `FunctionResultContent` trimming is unnecessary. If tool calling is terminated, the `FunctionResultContent` from the terminated call was never sent to a subsequent service call, so it is never persisted. The per-service-call approach naturally matches the service's behavior.
- **Per-run persistence**: When messages are batched and persisted at the end of the full run, trailing `FunctionResultContent` trimming becomes necessary to match the service's behavior. Without trimming, the stored history contains `FunctionResultContent` that the service would never have stored.
## Decision Drivers
- **A. Consistency**: The default behavior of `ChatHistoryProvider` should produce stored history that closely matches what the underlying AI service would store, minimizing surprise when switching between framework-managed and service-managed chat history.
- **B. Atomicity**: A run that fails mid-way through a multi-step tool-calling loop should not leave chat history in a partially-updated state, unless the user explicitly opts into that behavior.
- **C. Recoverability**: For long-running tool-calling loops, it should be possible to recover intermediate progress if the process is interrupted, rather than losing all work from the current run.
- **D. Simplicity**: The default behavior should be easy to understand and predict for most users, without requiring knowledge of the FIC loop internals.
- **E. Flexibility**: Regardless of the chosen default, users should be able to opt into the alternative behavior.
## Considered Options
- Option 1: Per-run persistence with opt-in FRC (FunctionResultContent) trimming
- Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)
## Pros and Cons of the Options
### Option 1: Per-run persistence with opt-in FRC trimming
Keep the current default behavior of persisting chat history only at the end of the full agent run. Add `FunctionResultContent` trimming as an opt-in behavior to improve consistency with service storage.
- Good, because runs are atomic — chat history is only updated when the full run succeeds, satisfying driver B.
- Good, because the mental model is simple: one run = one history update, satisfying driver D.
- Good, because trimming trailing `FunctionResultContent` improves consistency with service storage, partially satisfying driver A.
- Bad, because the default persistence timing still differs from the service's behavior (per-run vs. per-service-call), only partially satisfying driver A.
- Bad, because if the process crashes mid-loop, all intermediate progress from the current run is lost, not satisfying driver C.
- Bad, because this option alone does not provide a way for users to opt into per-service-call persistence, not satisfying driver E.
### Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)
Introduce an optional RequirePerServiceCallChatHistoryPersistence setting to persist chat history after each individual service call within the FIC loop, matching the AI service's behavior. Trailing `FunctionResultContent` trimming is unnecessary with this approach (it is naturally handled).
Settings:
- `RequirePerServiceCallChatHistoryPersistence` = `true`
- Good, because the stored history matches the service's behavior when opting in for both timing and content, fully satisfying driver A.
- Good, because intermediate progress is preserved if the process is interrupted, satisfying driver C.
- Good, because no separate `FunctionResultContent` trimming logic is needed, reducing complexity.
- Bad, because chat history may be left in an incomplete state if the run fails mid-loop (e.g., `FunctionCallContent` stored without corresponding `FunctionResultContent`), not satisfying driver B. A subsequent run cannot proceed without manually providing the missing `FunctionResultContent`.
- Bad, because the mental model is more complex: a single run may produce multiple history updates, partially failing driver D.
- Neutral, because users can opt out to per-run persistence if they prefer atomicity, satisfying driver E.
## Decision Outcome
Chosen option: **Option 2: Opt-in per-service-call persistence (via `RequirePerServiceCallChatHistoryPersistence`)**. The existing per-run persistence behavior is retained as-is, requiring no changes from users. Per-service-call persistence is available as an opt-in feature via the `RequirePerServiceCallChatHistoryPersistence` setting. This satisfies drivers B (atomicity) and D (simplicity) for the common case, while fully satisfying driver A (consistency) for users who opt into simulated service-stored behavior. Users who need per-service-call persistence for recoverability (driver C) can enable it explicitly.
### Configuration Matrix
The behavior depends on the combination of `UseProvidedChatClientAsIs` and `RequirePerServiceCallChatHistoryPersistence`:
| `UseProvidedChatClientAsIs` | `RequirePerServiceCallChatHistoryPersistence` | Behavior |
|---|---|---|
| `false` (default) | `false` (default) | **Per-run persistence.** Messages are persisted at the end of the full agent run via the `ChatHistoryProvider`. |
| `false` | `true` | **Per-service-call persistence (simulated).** A `PerServiceCallChatHistoryPersistingChatClient` middleware is automatically injected into the chat client pipeline between `FunctionInvokingChatClient` and the leaf `IChatClient`. Messages are persisted after each service call. A sentinel `ConversationId` causes FIC to treat the conversation as service-managed. |
| `true` | `false` | **Per-run persistence.** No middleware is injected because the user has provided a custom chat client stack. Messages are persisted at the end of the run. |
| `true` | `true` | **User responsibility.** The system checks whether the custom chat client stack includes a `PerServiceCallChatHistoryPersistingChatClient`. If not, a warning is emitted — the user is expected to have added their own per-service-call persistence mechanism. End-of-run persistence is skipped. |
### Consequences
- Good, because per-run persistence is atomic by default — chat history is only updated when the full run succeeds, satisfying driver B.
- Good, because the default mental model is simple: one run = one history update, satisfying driver D.
- Good, because users who opt into `RequirePerServiceCallChatHistoryPersistence` get stored history that matches the service's behavior for both timing and content, fully satisfying driver A.
- Good, because per-service-call persistence preserves intermediate progress if the process is interrupted, satisfying driver C when opted in.
- Good, because no separate `FunctionResultContent` trimming logic is needed when per-service-call persistence is active — it is naturally handled.
- Good, because conflict detection (configurable via `ThrowOnChatHistoryProviderConflict`, `WarnOnChatHistoryProviderConflict`, `ClearOnChatHistoryProviderConflict`) prevents misconfiguration when a service returns a `ConversationId` alongside a configured `ChatHistoryProvider`.
- Bad, because per-service-call persistence (when opted in) may leave chat history in an incomplete state if the run fails mid-loop (e.g., `FunctionCallContent` stored without corresponding `FunctionResultContent`), requiring manual recovery in rare cases.
- Neutral, because users who want per-service-call consistency can opt in via `RequirePerServiceCallChatHistoryPersistence = true`, satisfying driver E.
- Neutral, because increased write frequency from per-service-call persistence may impact performance for some storage backends; this can be mitigated with a caching decorator.
### Implementation Notes
#### Conversation ID Consistency
When `RequirePerServiceCallChatHistoryPersistence` is enabled, the `PerServiceCallChatHistoryPersistingChatClient`
decorator also updates `session.ConversationId` after each service call. This handles two scenarios:
1. **Framework-managed chat history** — the decorator sets a sentinel `ConversationId` on the response
so that `FunctionInvokingChatClient` treats the conversation as service-managed (clearing accumulated
history between iterations and not injecting duplicate `FunctionCallContent` during approval processing).
2. **Service-stored chat history** — when the service returns a real `ConversationId`, the decorator
updates `session.ConversationId` immediately after each service call, rather than deferring the update
to the end of the run. This ensures intermediate ConversationId changes are captured even if the
process is interrupted mid-loop.
For some service-stored scenarios (e.g., the Conversations API with the Responses API), there is only
one thread with one ID, so every service call returns the same ConversationId and this per-call update
makes no practical difference. Enabling `RequirePerServiceCallChatHistoryPersistence` ensures consistent
per-service-call behavior across all service types regardless of how they manage ConversationIds.
@@ -0,0 +1,815 @@
---
status: accepted
contact: bentho
date: 2026-02-27
deciders: bentho, markwallace-microsoft, westey-m
consulted: Pratyush Mishra, Shivam Shrivastava, Manni Arora (Centrica eval scenario)
informed: Agent Framework team, Foundry Evals team
---
# Agent Evaluation Architecture with Azure AI Foundry Integration
## Context and Problem Statement
Azure AI Foundry provides a rich evaluation service for AI agents — built-in evaluators for agent behavior (task adherence, intent resolution), tool usage (tool call accuracy, tool selection), quality (coherence, fluency, relevance), and safety (violence, self-harm, prohibited actions). Results are viewable in the Foundry portal with dashboards and comparison views.
However, using Foundry Evals with an agent-framework agent today requires significant manual effort. Developers must:
1. Transform agent-framework's `Message`/`Content` types into the OpenAI-style agent message schema that Foundry evaluators expect
2. Map tool definitions from agent-framework's `FunctionTool` format to evaluator-compatible schemas
3. Manually wire up the correct Foundry data source type (`azure_ai_traces`, `jsonl`, `azure_ai_target_completions`, etc.) depending on their scenario
4. Handle App Insights trace ID queries, response ID collection, and eval polling
Additionally, evaluation is a concern that extends beyond any single provider. Developers may want to use local evaluators (LLM-as-judge, regex, keyword matching), third-party evaluation libraries, or multiple providers in combination. The architecture must support this without creating a Foundry-specific lock-in at the API level.
### Functional Requirements for Agent Evaluation
- **Single agents and workflows.** Evaluate both individual agent responses and multi-agent workflow results, with per-agent breakdown to pinpoint underperformance.
- **One-shot and multi-turn conversations.** Capture full conversation trajectories — including tool calls and results — not just final query/response pairs.
- **Conversation factoring.** Support splitting conversations into query/response in multiple ways (last turn, full trajectory, per-turn) because different factorings measure different things.
- **Multiple providers, mix and match.** Run Foundry LLM-as-judge evaluators alongside fast local checks and custom evaluators on the same data, without restructuring code.
- **Third-party extensibility.** Any evaluation library can participate by implementing the `Evaluator` protocol (Python) or `IAgentEvaluator` interface (.NET). No predetermined list of supported libraries — the protocol is intentionally simple (`evaluate(items) → results`) so that wrappers for libraries like DeepEval, RAGAS, or Promptfoo are straightforward to write.
- **Bring your own evaluator.** Creating a custom evaluator should be as simple as writing a function.
- **Evaluate without re-running.** Evaluate existing responses from logs or previous runs without invoking the agent again.
## Decision Drivers
- **Zero-friction evaluation**: Developers should go from "I have an agent" to "I have eval results" with minimal code.
- **Provider-agnostic API**: Core evaluation capabilities must not be tied to any specific provider. Provider configuration should be separate from the evaluation call.
- **Lowest concept count**: Introduce the fewest possible new types, abstractions, and APIs for developers to learn.
- **Leverage existing knowledge**: The framework already knows which agents exist, what tools they have, and what conversations occurred. Evals should use this automatically rather than requiring the developer to re-specify it.
- **Foundry-native results**: When using Foundry, results should be viewable in the Foundry portal with dashboards and comparison views.
- **Progressive disclosure**: Simple scenarios should be near-zero code. Advanced scenarios should build on the same primitives.
- **Cross-language parity**: Design must be implementable in both Python and .NET.
## Considered Options
1. **Provider-specific functions** — Build Foundry-specific helper functions (`evaluate_agent()`, etc.) directly in the Azure package. All eval functions take Foundry connection parameters.
2. **Evaluator protocol with shared orchestration** — Define a provider-agnostic `Evaluator` protocol in the base agent library (`agent_framework` in Python, `Microsoft.Agents.AI` in .NET). Orchestration functions live alongside it. Providers implement the protocol.
3. **Full eval framework** — Build comprehensive eval infrastructure including custom evaluator definitions, scoring profiles, and reporting inside agent-framework.
## Decision Outcome
Proposed option: "Evaluator protocol with shared orchestration", because it delivers the low-friction developer experience, supports multiple providers without API changes, and keeps the concept count low.
### Usage Examples
#### Evaluate an agent
The agent is invoked once per query by default. For statistically meaningful evaluation, provide multiple diverse queries. For measuring **consistency** (does the same query produce reliable results?), use `num_repetitions` to run each query N times independently:
**Python:**
```python
evals = FoundryEvals(
project_client=client,
model_deployment="gpt-4o",
evaluators=[FoundryEvals.RELEVANCE, FoundryEvals.COHERENCE],
)
results = await evaluate_agent(
agent=my_agent,
queries=[
"What's the weather in Seattle?",
"Plan a weekend trip to Portland",
"What restaurants are near Pike Place?",
],
evaluators=evals,
)
for r in results:
r.assert_passed()
```
**C#:**
```csharp
var evals = new FoundryEvals(chatConfiguration, FoundryEvals.Relevance, FoundryEvals.Coherence);
AgentEvaluationResults results = await agent.EvaluateAsync(
new[] {
"What's the weather in Seattle?",
"Plan a weekend trip to Portland",
"What restaurants are near Pike Place?",
},
evals);
results.AssertAllPassed();
```
`evaluate_agent` returns one `EvalResults` per evaluator. Each result contains per-item scores with the evaluated response for auditing:
```
# results[0] (FoundryEvals)
EvalResults(status="completed", passed=3, failed=0, total=3)
items[0]: EvalItemResult(
query="What's the weather in Seattle?",
response="It's currently 72°F and sunny in Seattle.",
scores={"relevance": 5, "coherence": 5})
items[1]: EvalItemResult(
query="Plan a weekend trip to Portland",
response="Here's a 2-day Portland itinerary...",
scores={"relevance": 4, "coherence": 5})
items[2]: EvalItemResult(
query="What restaurants are near Pike Place?",
response="Top restaurants near Pike Place Market: ...",
scores={"relevance": 5, "coherence": 4})
```
#### Measure consistency with repetitions
Run each query multiple times to detect non-deterministic behavior:
**Python:**
```python
results = await evaluate_agent(
agent=my_agent,
queries=["What's the weather in Seattle?"],
evaluators=evals,
num_repetitions=3, # each query runs 3 times independently
)
# results contain 3 items (1 query × 3 repetitions)
```
**C#:**
```csharp
AgentEvaluationResults results = await agent.EvaluateAsync(
new[] { "What's the weather in Seattle?" },
evals,
numRepetitions: 3); // each query runs 3 times independently
// results contain 3 items (1 query × 3 repetitions)
```
#### Evaluate a response you already have
When you already have agent responses, pass them directly to skip re-running the agent. Each query is paired with its corresponding response:
**Python:**
```python
queries = ["What's the weather?", "What's the capital of France?"]
responses = [await agent.run([Message("user", [q])]) for q in queries]
results = await evaluate_agent(
responses=responses,
evaluators=evals,
)
```
**C#:**
```csharp
var queries = new[] { "What's the weather?" };
var responses = new List<AgentResponse>();
foreach (var q in queries)
responses.Add(await agent.RunAsync(new[] { new ChatMessage(ChatRole.User, q) }));
AgentEvaluationResults results = await agent.EvaluateAsync(
responses: responses,
evals);
```
Each `AgentResponse` already contains the conversation (query + response), so the evaluator extracts query/response from the conversation. When you pass `responses` without `queries`, the conversation is the source of truth.
#### Evaluate with conversation split strategies
By default, evaluators see only the last turn (final user message → final assistant response). For multi-turn conversations, you can control how the conversation is factored for evaluation:
**Python:**
```python
results = await evaluate_agent(
agent=agent,
queries=["Plan a 3-day trip to Paris"],
evaluators=evals,
conversation_split=ConversationSplit.FULL, # evaluate entire trajectory
)
# Or per-turn: each user→assistant exchange scored independently
results = await evaluate_agent(
agent=agent,
queries=["Plan a 3-day trip to Paris"],
evaluators=evals,
conversation_split=ConversationSplit.PER_TURN,
)
```
**C#:**
```csharp
// Full conversation as context
AgentEvaluationResults results = await agent.EvaluateAsync(
new[] { "Plan a 3-day trip to Paris" },
evals,
splitter: ConversationSplitters.Full);
// Per-turn splitting
var items = EvalItem.PerTurnItems(conversation); // one EvalItem per user turn
var results = await evals.EvaluateAsync(items);
```
With `PER_TURN`, a 3-turn conversation produces 3 scored items:
```
EvalResults(status="completed", passed=3, failed=0, total=3)
items[0]: query="Plan a 3-day trip to Paris" scores={"relevance": 5}
items[1]: query="What about restaurants?" scores={"relevance": 4}
items[2]: query="Make it budget-friendly" scores={"relevance": 5}
```
#### Evaluate a multi-agent workflow
**Python:**
```python
result = await workflow.run("Plan a trip to Paris")
eval_results = await evaluate_workflow(
workflow=workflow,
workflow_result=result,
evaluators=evals,
)
for r in eval_results:
print(f" overall: {r.passed}/{r.total}")
for name, sub in r.sub_results.items():
print(f" {name}: {sub.passed}/{sub.total}")
```
**C#:**
```csharp
WorkflowRunResult result = await workflow.RunAsync("Plan a trip to Paris");
IReadOnlyList<AgentEvaluationResults> evalResults = await result.EvaluateAsync(evals);
foreach (var r in evalResults)
{
Console.WriteLine($" overall: {r.Passed}/{r.Total}");
foreach (var (name, sub) in r.SubResults)
Console.WriteLine($" {name}: {sub.Passed}/{sub.Total}");
}
```
Workflows return one result per evaluator, with sub-results per agent in the workflow:
```
EvalResults(status="completed", passed=2, failed=0, total=2)
sub_results:
"planner": EvalResults(passed=1, total=1)
"researcher": EvalResults(passed=1, total=1)
```
#### Mix multiple providers
**Python:**
```python
@evaluator
def is_helpful(response: str) -> bool:
return len(response.split()) > 10
foundry = FoundryEvals(
project_client=client,
model_deployment="gpt-4o",
evaluators=[FoundryEvals.RELEVANCE, FoundryEvals.COHERENCE],
)
results = await evaluate_agent(
agent=agent,
queries=queries,
evaluators=[is_helpful, keyword_check("weather"), foundry],
)
```
**C#:**
```csharp
IReadOnlyList<AgentEvaluationResults> results = await agent.EvaluateAsync(
queries,
evaluators: new IAgentEvaluator[]
{
new LocalEvaluator(
EvalChecks.KeywordCheck("weather"),
FunctionEvaluator.Create("is_helpful", (string r) => r.Split(' ').Length > 10)),
new FoundryEvals(chatConfiguration, FoundryEvals.Relevance, FoundryEvals.Coherence),
});
```
Multiple evaluators return one result each — `results[0]` is the local evaluator, `results[1]` is Foundry.
#### Custom function evaluators
**Python:**
```python
@evaluator
def mentions_city(response: str, expected_output: str) -> bool:
return expected_output.lower() in response.lower()
@evaluator
def used_tools(conversation: list, tools: list) -> float:
# ... scoring logic
return score
local = LocalEvaluator(mentions_city, used_tools)
```
`@evaluator` uses **parameter name injection** — the function's parameter names determine what data it receives from the `EvalItem`. Supported names: `query`, `response`, `expected`, `expected_tool_calls`, `conversation`, `tools`, `context`. Any combination is valid.
**C#:**
```csharp
var local = new LocalEvaluator(
FunctionEvaluator.Create("mentions_city",
(EvalItem item) => item.ExpectedOutput != null
&& item.Response.Contains(item.ExpectedOutput, StringComparison.OrdinalIgnoreCase)),
FunctionEvaluator.Create("is_concise",
(string response) => response.Split(' ').Length < 500));
```
## What To Build
### Core: Evaluator Protocol
A runtime-checkable protocol that any evaluation provider implements:
```python
@runtime_checkable
class Evaluator(Protocol):
name: str
async def evaluate(
self, items: Sequence[EvalItem], *, eval_name: str = "Agent Framework Eval"
) -> EvalResults: ...
```
The protocol is minimal — just `name` and `evaluate()`.
### Core: EvalItem
Provider-agnostic data format for items to evaluate:
```python
@dataclass
class ExpectedToolCall:
name: str # Tool/function name
arguments: dict[str, Any] | None = None # None = don't check args
@dataclass
class EvalItem:
conversation: list[Message] # Single source of truth
tools: list[FunctionTool] | None = None # Agent's available tools
context: str | None = None
expected_output: str | None = None # Ground-truth for comparison
expected_tool_calls: list[ExpectedToolCall] | None = None
split_strategy: ConversationSplitter | None = None
query: str # property — derived from conversation split
response: str # property — derived from conversation split
```
`conversation` is the single source of truth. `query` and `response` are derived properties — splitting the conversation at the last user message (default) and extracting text from each side. Changing the `split_strategy` consistently changes all derived values.
`tools` provides typed `FunctionTool` objects — including MCP tools, which are automatically extracted after agent runs.
### Internal: AgentEvalConverter
Internal class that converts agent-framework types to `EvalItem`. Used by `evaluate_agent()` and `evaluate_workflow()` — not part of the public API:
| Agent Framework | Eval Format |
|---|---|
| `Content.function_call` | `tool_call` in OpenAI chat format |
| `Content.function_result` | `tool_result` in OpenAI chat format |
| `FunctionTool` | `{name, description, parameters}` schema |
| `Message` history | `conversation` list + `query`/`response` extraction |
### Core: EvalResults
Rich result type with convenience properties for CI integration:
```python
results.all_passed # bool: no failures or errors (recursive for workflow)
results.passed # int: passing count
results.failed # int: failure count
results.total # int: total = passed + failed + errored
results.items # list[EvalItemResult]: per-item detail with query, response, and scores
results.error # str | None: error details on failure
results.sub_results # dict: per-agent breakdown (workflow evals)
results.report_url # str | None: portal link (Foundry)
results.assert_passed() # raises AssertionError with details
```
### Core: Orchestration Functions
Provider-agnostic functions that extract data and delegate to evaluators:
| Function | What it does |
|---|---|
| `evaluate_agent()` | Runs agent against test queries (or evaluates pre-existing `responses=`), converts to `EvalItem`s, passes to evaluator. Accepts optional `expected_output=` for ground-truth comparison, `expected_tool_calls=` for tool-correctness evaluation, and `num_repetitions=` for consistency measurement |
| `evaluate_workflow()` | Extracts per-agent data from `WorkflowRunResult`, evaluates each agent and overall output. Per-agent breakdown in `sub_results`. Also accepts `num_repetitions=` |
### Core: Conversation Split Strategies
Multi-turn conversations must be split into query (input) and response (output) halves for evaluation. How you split determines *what you're evaluating*:
**Last-turn split** — split at the last user message. Everything up to and including it is the query context; the agent's subsequent actions are the response:
```
conversation: user1 → assistant1 → user2 → assistant2(tool) → tool_result → assistant3
query_messages: [user1, assistant1, user2]
response_messages: [assistant2(tool), tool_result, assistant3]
```
This evaluates: "Given all the context so far, did the agent answer the latest question well?" Best for response quality at a specific point in the conversation.
**Full-conversation split** — the first user message is the query; everything after is the response:
```
query_messages: [user1]
response_messages: [assistant1, user2, assistant2(tool), tool_result, assistant3]
```
This evaluates: "Given the original request, did the entire conversation trajectory serve the user?" Best for task completion and overall conversation quality.
**Per-turn split** — produces N eval items from an N-turn conversation. Each turn is evaluated with its cumulative context:
```
item 1: query = [user1], response = [assistant1]
item 2: query = [user1, assistant1, user2], response = [assistant2(tool), tool_result, assistant3]
```
This evaluates each response independently. Best for fine-grained analysis and pinpointing where a conversation goes wrong.
These factorings produce different scores for the same conversation. The framework ships all three as built-in strategies, defaulting to last-turn. Developers can also provide a custom splitter — a function (Python) or `IConversationSplitter` implementation (.NET) — and override the strategy at the call site or per evaluator.
### Azure AI: FoundryEvals
`Evaluator` implementation backed by Azure AI Foundry:
```python
class FoundryEvals:
def __init__(self, *, project_client=None, openai_client=None,
model_deployment: str, evaluators=None, ...)
async def evaluate(self, items, *, eval_name) -> EvalResults
```
**Smart auto-detection in `evaluate()`:**
- Default evaluators: relevance, coherence, task_adherence
- Auto-adds `tool_call_accuracy` when items have tools/`tool_definitions`
- Filters out tool evaluators for items without tools
### Azure AI: FoundryEvals Constants
```python
from agent_framework.foundry import FoundryEvals
evaluators = [FoundryEvals.RELEVANCE, FoundryEvals.TOOL_CALL_ACCURACY]
```
Categories: Agent behavior, Tool usage, Quality, Safety.
### Azure AI: Foundry-Specific Functions
| Function | What it does |
|---|---|
| `evaluate_traces()` | Evaluate from stored response IDs or OTel traces |
| `evaluate_foundry_target()` | Evaluate a Foundry-registered agent or deployment |
### Core: LocalEvaluator and Function Evaluators
`LocalEvaluator` implements the `Evaluator` protocol for fast, API-free evaluation. It runs check functions locally — useful for inner-loop development, CI smoke tests, and combining with cloud-based evaluators.
Built-in checks:
- `keyword_check(*keywords)` — response must contain specified keywords
- `tool_called_check(*tool_names)` — agent must have called specified tools
- `tool_calls_present` — all `expected_tool_calls` names appear in conversation (unordered, extras OK)
- `tool_call_args_match` — expected tool calls match on name + arguments (subset match on args)
Custom function evaluators use `@evaluator` to wrap plain Python functions. The function's **parameter names** determine what data it receives from the `EvalItem`:
```python
from agent_framework import evaluator, LocalEvaluator
# Tier 1: Simple check — just query + response
@evaluator
def is_concise(response: str) -> bool:
return len(response.split()) < 500
# Tier 2: Ground truth — compare against expected output
@evaluator
def mentions_city(response: str, expected_output: str) -> bool:
return expected_output.lower() in response.lower()
# Tier 3: Full context — inspect conversation and tools
@evaluator
def used_tools(conversation: list, tools: list) -> float:
# ... scoring logic
return score
local = LocalEvaluator(is_concise, mentions_city, used_tools)
```
Supported parameters: `query`, `response`, `expected`, `expected_tool_calls`, `conversation`, `tools`, `context`.
Return types: `bool`, `float` (≥0.5 = pass), `dict` with `score` or `passed` key, or `CheckResult`.
Async functions are handled automatically — `@evaluator` detects `async def` and produces the right wrapper.
### Example: GAIA Benchmark
[GAIA](https://huggingface.co/gaia-benchmark) tests real-world multi-step tasks with known expected answers. Each task has a question and a ground-truth answer, with optional file attachments. The framework accommodates GAIA's knobs (difficulty levels, file inputs, multi-step tool use) through the existing `EvalItem` fields:
```python
from datasets import load_dataset
from agent_framework import evaluate_agent, evaluator, LocalEvaluator
gaia = load_dataset("gaia-benchmark/GAIA", "2023_level1", split="test")
@evaluator
def exact_match(response: str, expected_output: str) -> bool:
return expected_output.strip().lower() in response.strip().lower()
# Simple path — evaluate_agent handles running + expected_output stamping
results = await evaluate_agent(
agent=agent,
queries=[task["Question"] for task in gaia],
expected_output=[task["Final answer"] for task in gaia],
evaluators=LocalEvaluator(exact_match),
)
```
### Package Location
- Core types and orchestration: `agent_framework._eval`, `agent_framework._local_eval` (Python), `Microsoft.Agents.AI` (.NET)
- Foundry provider: `agent_framework_azure_ai._foundry_evals` (Python), `Microsoft.Agents.AI.AzureAI` (.NET)
- Azure-AI re-exports core types for convenience (Python)
## Known Limitations
1. **Tool evaluators require query + agent**: Tool evaluators need tool definition schemas. When using these evaluators with `evaluate_agent(responses=...)`, provide `queries=` and pass an agent with tool definitions.
2. **`model_deployment` always required**: Could potentially be inferred from the Foundry project configuration.
## Open Questions
1. **Red teaming non-registered agents**: Requires Foundry API support for callback-based flows.
2. **Datasets with expected outputs**: A dataset abstraction for pre-populating `expected_output` values across eval runs is a natural next step but not yet designed.
3. **Multi-modal evaluation**: The `conversation` field on `EvalItem` already stores full `Message`/`Content` (Python) and `ChatMessage` (.NET) objects, which can represent multi-modal content (images, audio, structured data). Evaluators that accept the full `EvalItem` or `conversation` parameter can access this content today. However, the convenience shortcuts — `query`/`response` string projections and the `FunctionEvaluator` string overloads — are text-only. Multi-modal-aware evaluators should use the full-item path (`Func<EvalItem, CheckResult>` in .NET, `conversation: list` parameter in Python).
## .NET Implementation Design
### Key Difference: MEAI Ecosystem
Unlike Python, the .NET ecosystem already has `Microsoft.Extensions.AI.Evaluation` (v10.3.0) providing:
- `IEvaluator` — per-item evaluation of `(messages, chatResponse) → EvaluationResult`
- `CompositeEvaluator` — combines multiple evaluators
- Quality evaluators — `RelevanceEvaluator`, `CoherenceEvaluator`, `GroundednessEvaluator`
- Safety evaluators — `ContentHarmEvaluator`, `ProtectedMaterialEvaluator`
- Metric types — `NumericMetric`, `BooleanMetric`, `StringMetric`
The .NET integration uses MEAI's `IEvaluator` directly — no new evaluator interface. Our contribution is the **orchestration layer**: extension methods that run agents, extract data, call `IEvaluator` per item, and aggregate results.
### Architecture
```
┌──────────────────────────────────────────────────────────────┐
│ Developer Code │
│ agent.EvaluateAsync(queries, evaluator) │
│ run.EvaluateAsync(evaluator) │
└────────────────┬─────────────────────────────────────────────┘
┌────────────────▼─────────────────────────────────────────────┐
│ Orchestration Layer (Microsoft.Agents.AI) │
│ AgentEvaluationExtensions — runs agents, extracts data, │
│ calls IEvaluator per item, aggregates into │
│ AgentEvaluationResults │
└────────────────┬─────────────────────────────────────────────┘
│ IEvaluator (MEAI)
┌───────────┼────────────┐
│ │ │
┌───▼───-┐ ┌───▼────┐ ┌────▼──────────┐
│ MEAI │ │ Local │ │ Foundry │
│ Quality│ │ Checks │ │ (cloud batch) │
│ Safety │ │ Lambdas│ │ │
└────────┘ └────────┘ └───────────────┘
```
All evaluators implement MEAI's `IEvaluator`. The orchestration layer doesn't need to know which kind — it calls `EvaluateAsync(messages, chatResponse)` per item on all of them. `FoundryEvals` handles batching internally (buffers items, submits once, returns per-item results).
### .NET Core Types
**No new evaluator interface.** Use MEAI's `IEvaluator` directly.
**`AgentEvaluationResults`** — The only new type. Aggregates per-item MEAI `EvaluationResult`s across a batch of queries:
```csharp
public class AgentEvaluationResults
{
public string Provider { get; init; }
public string? ReportUrl { get; init; }
// Per-item — standard MEAI EvaluationResult, unchanged
public IReadOnlyList<EvaluationResult> Items { get; init; }
// Aggregate pass/fail derived from metric interpretations
public int Passed { get; }
public int Failed { get; }
public int Total { get; }
public bool AllPassed { get; }
// Workflow: per-agent breakdown
public IReadOnlyDictionary<string, AgentEvaluationResults>? SubResults { get; init; }
public void AssertAllPassed(string? message = null);
}
```
### .NET Evaluator Implementations
All implement MEAI's `IEvaluator`:
**`LocalEvaluator`** — Runs lambda checks locally, returns `BooleanMetric` per check:
```csharp
var local = new LocalEvaluator(
FunctionEvaluator.Create("is_concise",
(string response) => response.Split().Length < 500),
EvalChecks.KeywordCheck("weather"),
EvalChecks.ToolCalledCheck("get_weather"));
```
**MEAI evaluators** — Used directly, no adapter needed:
```csharp
var quality = new CompositeEvaluator(
new RelevanceEvaluator(),
new CoherenceEvaluator());
```
**`FoundryEvals`** — Implements `IEvaluator` but batches internally. On first call, buffers the item. On the last item (or when explicitly flushed), submits the batch to Foundry and distributes per-item results:
```csharp
var foundry = new FoundryEvals(projectClient, "gpt-4o");
```
### .NET Orchestration: Extension Methods
```csharp
public static class AgentEvaluationExtensions
{
// Evaluate an agent against test queries
public static Task<AgentEvaluationResults> EvaluateAsync(
this AIAgent agent,
IEnumerable<string> queries,
IEvaluator evaluator,
ChatConfiguration? chatConfiguration = null,
IEnumerable<string>? expectedOutput = null,
CancellationToken cancellationToken = default);
// Evaluate pre-existing responses (without re-running the agent)
public static Task<AgentEvaluationResults> EvaluateAsync(
this AIAgent agent,
AgentResponse responses,
IEvaluator evaluator,
IEnumerable<string>? queries = null,
ChatConfiguration? chatConfiguration = null,
IEnumerable<string>? expectedOutput = null,
CancellationToken cancellationToken = default);
// Evaluate with multiple evaluators (one result per evaluator)
public static Task<IReadOnlyList<AgentEvaluationResults>> EvaluateAsync(
this AIAgent agent,
IEnumerable<string> queries,
IEnumerable<IEvaluator> evaluators,
ChatConfiguration? chatConfiguration = null,
IEnumerable<string>? expectedOutput = null,
CancellationToken cancellationToken = default);
// Evaluate a workflow run with per-agent breakdown
public static Task<AgentEvaluationResults> EvaluateAsync(
this Run run,
IEvaluator evaluator,
ChatConfiguration? chatConfiguration = null,
bool includeOverall = true,
bool includePerAgent = true,
CancellationToken cancellationToken = default);
}
```
**Usage:**
```csharp
// MEAI evaluators — just works
var results = await agent.EvaluateAsync(
queries: ["What's the weather?"],
evaluator: new RelevanceEvaluator(),
chatConfiguration: new ChatConfiguration(evalClient));
// Local checks
var results = await agent.EvaluateAsync(
queries: ["What's the weather?"],
evaluator: new LocalEvaluator(
EvalChecks.KeywordCheck("weather")));
// Foundry cloud
var results = await agent.EvaluateAsync(
queries: ["What's the weather?"],
evaluator: new FoundryEvals(projectClient, "gpt-4o"));
// Evaluate existing response (without re-running the agent)
var response = await agent.RunAsync("What's the weather?");
var results = await agent.EvaluateAsync(
responses: response,
queries: ["What's the weather?"],
evaluator: new FoundryEvals(projectClient, "gpt-4o"));
// Mixed — one result per evaluator
var results = await agent.EvaluateAsync(
queries: ["What's the weather?"],
evaluators: [
new LocalEvaluator(EvalChecks.KeywordCheck("weather")),
new RelevanceEvaluator(),
new FoundryEvals(projectClient, "gpt-4o")
],
chatConfiguration: new ChatConfiguration(evalClient));
// Workflow with per-agent breakdown
Run run = await workflowRunner.RunAsync(workflow, "Plan a trip");
var results = await run.EvaluateAsync(
evaluator: new FoundryEvals(projectClient, "gpt-4o"));
```
### .NET Function Evaluators
Typed factory overloads (C# equivalent of Python's `@evaluator`):
```csharp
public static class FunctionEvaluator
{
public static EvalCheck Create(string name, Func<string, bool> check); // response only
public static EvalCheck Create(string name, Func<string, string?, bool> check); // expectedOutput
public static EvalCheck Create(string name, Func<EvalItem, bool> check); // full item
public static EvalCheck Create(string name, Func<EvalItem, CheckResult> check); // full control
public static EvalCheck Create(string name, Func<string, Task<bool>> check); // async
}
```
`EvalItem` is a lightweight record used only by `FunctionEvaluator` and `LocalEvaluator` to pass context to check functions. It is not part of the `IEvaluator` interface:
```csharp
public record ExpectedToolCall(string Name, IReadOnlyDictionary<string, object>? Arguments = null);
public sealed class EvalItem
{
public EvalItem(string query, string response, IReadOnlyList<ChatMessage> conversation);
public string Query { get; }
public string Response { get; }
public IReadOnlyList<ChatMessage> Conversation { get; }
public IReadOnlyList<AITool>? Tools { get; set; }
public string? ExpectedOutput { get; set; }
public IReadOnlyList<ExpectedToolCall>? ExpectedToolCalls { get; set; }
public string? Context { get; set; }
public IConversationSplitter? Splitter { get; set; }
}
```
### Workflow Data Extraction (.NET)
`run.EvaluateAsync()` walks `Run.OutgoingEvents` via LINQ:
1. Pair `ExecutorInvokedEvent` / `ExecutorCompletedEvent` by `ExecutorId`
2. Extract `AgentResponseEvent` for per-agent `ChatResponse`
3. Call `evaluator.EvaluateAsync()` per invocation
4. Group by `ExecutorId` for per-agent `SubResults`
5. Use final workflow output for overall eval
### .NET Package Structure
| Package | Contents |
|---------|----------|
| `Microsoft.Agents.AI` | `IAgentEvaluator`, `AgentEvaluationResults`, `LocalEvaluator`, `FunctionEvaluator`, `EvalChecks`, `EvalItem`, `ExpectedToolCall`, `AgentEvaluationExtensions` |
| `Microsoft.Agents.AI.AzureAI` | `FoundryEvals` (provider + constants) |
### Python ↔ .NET Mapping
| Python | .NET |
|--------|------|
| `Evaluator` protocol | `IAgentEvaluator` (our interface; MEAI provides `IEvaluator` for per-item scoring) |
| `EvalItem` dataclass | `EvalItem` class |
| `EvalResults` | `AgentEvaluationResults` |
| `EvalItemResult` / `EvalScoreResult` | MEAI `EvaluationResult` / `EvaluationMetric` (reused) |
| `LocalEvaluator` | `LocalEvaluator` (implements `IAgentEvaluator`) |
| `@evaluator` | `FunctionEvaluator.Create()` overloads |
| `keyword_check()` / `tool_called_check()` | `EvalChecks.KeywordCheck()` / `EvalChecks.ToolCalledCheck()` |
| `tool_calls_present` / `tool_call_args_match` | (custom `FunctionEvaluator` — same pattern) |
| `ExpectedToolCall` dataclass | `ExpectedToolCall` record |
| `FoundryEvals` | `FoundryEvals` (implements `IAgentEvaluator`, includes evaluator name constants) |
| `evaluate_agent()` | `agent.EvaluateAsync(queries, evaluator)` extension method |
| `evaluate_agent(responses=)` | `agent.EvaluateAsync(responses, evaluator)` extension method |
| `evaluate_workflow()` | `run.EvaluateAsync()` extension method |
## More Information
- [Foundry Evals documentation](https://learn.microsoft.com/azure/ai-foundry/concepts/evaluation-approach-gen-ai) — Azure AI Foundry evaluation overview
+233
View File
@@ -0,0 +1,233 @@
---
status: proposed
contact: eavanvalkenburg
date: 2026-04-07
deciders: TBD
consulted:
informed:
---
# CodeAct integration through backend-specific context providers and an `execute_code` tool
## Introduction
**CodeAct** is a pattern in which the model writes executable code — rather than emitting a fixed function-call JSON schema — to plan, transform data, and orchestrate tool calls inside a single sandbox invocation. Instead of requiring a separate model round-trip for every tool call, conditional branch, or data transformation, the model produces a short program that runs in a controlled runtime, calls host-provided tools through a `call_tool(...)` bridge, and returns structured results. This reduces latency, lowers token cost, and lets the model express richer multi-step logic that is difficult to capture in a flat tool-call sequence.
Throughout this ADR, **CodeAct** is the primary term. **Code mode** and **programmatic tool calling** refer to the same capability.
## Context and Problem Statement
We need an architecture design that supports CodeAct in both Python and .NET. This is a necessary capability for the current generation of long-running agents, which need to plan, iterate, transform tool outputs, and execute bounded code inside a controlled runtime — for example, filtering a large result set, computing derived values, or chaining several tool calls with conditional logic — instead of requiring a separate model round-trip for each of those steps. The design should preserve the same behavioral contract across SDKs, but it does not need to use the same internal extension point in each runtime. We also want to standardize on Hyperlight as the initial backend, using the existing Python package and an anticipated .NET binding package once it is available.
Throughout this ADR, **CodeAct** is the primary term. **Code mode** and **programmatic tool calling** refer to the same capability. This ADR uses **CodeAct** consistently.
Model-generated code is treated as untrusted relative to the host process. This ADR assumes the selected backend provides the primary isolation boundary, while the framework is responsible for configuring approvals and capabilities, integrating telemetry, and translating outputs and failures into framework-native shapes. If a backend cannot provide isolation appropriate for its trust model, it is not a suitable CodeAct backend.
The core design question is: **where should CodeAct integrate into the agent pipeline so that both SDKs can offer the same functionality without invasive changes to their core function-calling loops?**
## Decision Drivers
- CodeAct must shape the model-facing surface before model invocation, not only after the model has already chosen tools.
- The design should let users control which tools are available through CodeAct and which remain regular tools only.
- The design must preserve existing session, approval, telemetry, and tool invocation behavior as much as possible.
- The design should define the minimum cross-SDK telemetry and failure semantics for `execute_code`, so Python and .NET do not diverge on basic observability or error handling.
- The design must fit naturally into the extension points that already exist in each SDK.
- The design must be safe for concurrent runs and must not rely on mutating shared agent configuration during invocation.
- The chosen structure should allow multiple backend-specific providers to fit under the same conceptual design over time, even though Hyperlight is the initial target.
- The abstraction should not assume that every backend is a VM-style sandbox; alternative execution models such as Pydantic's Monty should also fit.
- The design should allow `execute_code` to be reused both as a tool-enabled CodeAct runtime and as a standard code interpreter tool implementation.
- The design should remain open to alternative language/runtime modes, such as JavaScript on Hyperlight, rather than baking the abstraction to Python only.
- The design should provide a portable way to configure sandbox capabilities such as file access and network access, including allow-listed outbound domains.
- Using CodeAct should be optional, and installing its runtime or backend dependencies should also be optional.
- Backend-specific dependencies should be isolated behind a small adapter so SDK code is not tightly coupled to an unstable package surface.
## Considered Options
- **Option 1**: Standardize on context provider-based CodeAct with a shared cross-SDK contract and backend-specific public types
- **Option 2**: Implement CodeAct as a dedicated chat-client decorator/wrapper
- **Option 3**: Integrate CodeAct directly into the function invocation layer/FunctionInvokingChatClient
## Pros and Cons of the Options
### Option 1: Standardize on context provider-based CodeAct with a shared cross-SDK contract and backend-specific public types
This option uses `ContextProvider` in Python and `AIContextProvider` in .NET, but standardizes the public concept and behavior.
In this option, the CodeAct tool set is provider-owned: only tools explicitly configured on the concrete CodeAct provider instance are available inside CodeAct, and the provider exposes direct CRUD-style management for tools, file mounts, and outbound network allow-list configuration rather than requiring a separate runtime setup object.
The agent's direct tool surface remains separate. If a tool should be available both through CodeAct and as a normal direct tool, it is configured in both places.
- Good, because both SDKs already have first-class provider concepts intended for per-invocation context shaping.
- Good, because providers operate before model invocation, which is where CodeAct must add instructions and reshape tools.
- Good, because this lets us preserve existing function invocation behavior rather than rewriting it.
- Good, because slightly different internals are acceptable while the public behavior remains aligned.
- Good, because convenience builder/decorator helpers can still be added later on top of the provider model without changing the core design.
- Good, because backend-specific runtime logic can stay inside concrete provider implementations or internal helpers instead of being forced into a lowest-common-denominator public abstraction.
- Good, because the same provider structure can support either an all-or-nothing tool surface or a mixed side-by-side tool surface.
- Good, because users can keep some tools direct-only while allowing other tools to be used from inside CodeAct.
- Good, because a provider-owned CodeAct tool registry avoids mutating or inferring the agent's direct tool surface and can work consistently in both SDKs.
- Good, because the same conceptual design can remain open to `HyperlightCodeActProvider`, a future `MontyCodeActProvider`, and other backend-specific providers over time.
- Good, because `execute_code` can evolve into multiple backend-specific runtime modes rather than being hard-wired to one Python-plus-tools mode.
- Bad, because the provider indirection adds per-run overhead — snapshotting the tool registry, dispatching lifecycle hooks, and building instructions — that a deeper integration point could skip. In practice this overhead is negligible relative to model inference latency and sandbox startup cost.
### Option 2: Implement CodeAct as a dedicated chat-client decorator/wrapper
This option would introduce a CodeAct-specific chat-client decorator that injects instructions and tools directly into the chat request pipeline.
- Good, because this is a natural fit for .NET's `DelegatingChatClient` pipeline.
- Good, because it can also support advanced custom chat-client stacks.
- Good, because backend-specific runtime selection could be hidden inside the decorator implementation.
- Good, because the decorator could also encapsulate mode-specific instruction shaping for tool-enabled versus standalone interpreter behavior.
- Good, because the decorator can decide per request whether the tool surface is exclusive or mixed.
- Bad, because Python can support this by building a custom layering stack on top of a `Raw...Client` and swapping in a different `FunctionInvocationLayer`, but that composition path is more manual than the .NET `DelegatingChatClient` pipeline.
- Bad, because it duplicates responsibilities already handled by provider abstractions.
- Bad, because it makes CodeAct look more transport-specific than it really is.
- Bad, because swappable backends and reusable interpreter or language modes become coupled to chat-client composition rather than modeled as first-class CodeAct concepts.
### Option 3: Integrate CodeAct directly into the function invocation layer/FunctionInvokingChatClient
This option would push CodeAct into Python's `FunctionInvocationLayer` and .NET's `FunctionInvokingChatClient` or related middleware.
- Good, because it is close to tool execution and can observe concrete tool invocation behavior.
- Good, because function middleware may still be useful later for auxiliary auditing or policy around sandbox-originated tool calls.
- Bad, because this is the wrong layer for constructing the model-facing tool surface and prompt instructions.
- Bad, because it does not naturally control whether the model sees an exclusive CodeAct tool surface or a mixed side-by-side tool surface.
- Bad, because it would still require a second mechanism for hiding normal tools and advertising `execute_code`.
- Bad, because it is a weak fit for standalone interpreter modes where no tool-calling loop is needed.
- Bad, because backend selection and CodeAct mode behavior are orthogonal concerns that do not belong in the function invocation layer.
- Bad, because `.NET` would become more tightly coupled to `FunctionInvokingChatClient`, which sits below the agent framework abstraction and is not the natural cross-SDK design seam.
## Approval Model Options
- **Option A**: Bundled approval for the `execute_code` invocation
- **Option B**: Pre-execution inspection of `call_tool(...)` references before approving `execute_code`
- **Option C**: Nested per-tool approvals during `execute_code`
## Pros and Cons of the Approval Options
### Option A: Bundled approval for the `execute_code` invocation
This option grants approval once, before `execute_code` starts. Provider-owned tool calls made from inside that execution run under the same approval. The effective approval of `execute_code` is determined up front from the provider configuration rather than from inspecting which tools are actually called during execution.
- Good, because it is the simplest model to explain and implement consistently in both SDKs.
- Good, because it fits naturally with long-running CodeAct loops where repeated approval interruptions would be disruptive.
- Good, because it does not require static code analysis before execution begins.
- Good, because it keeps the first release focused on the provider integration rather than a more complex approval engine.
- Bad, because approval is coarse-grained and may cover more activity than the user expected.
- Bad, because it provides less visibility into which provider-owned tools or capabilities will be exercised during the run.
### Option B: Pre-execution inspection of `call_tool(...)` references before approving `execute_code`
This option inspects submitted code for statically discoverable `call_tool("tool_name", ...)` references before execution starts and uses that information to shape the approval request.
- Good, because it can show users more detail up front while still keeping approval at a single pre-execution moment.
- Good, because it matches the common case where tool names are spelled out directly in the generated code.
- Good, because it can coexist with bundled approval as a more informative variant of the same UX.
- Bad, because the analysis is inherently best-effort and cannot reliably predict dynamic behavior.
- Bad, because it requires duplicated parsing or inspection logic that does not replace runtime enforcement.
### Option C: Nested per-tool approvals during `execute_code`
This option requests approval when sandboxed code actually attempts to invoke a provider-owned tool that requires approval.
- Good, because it aligns approval with real behavior rather than predicted behavior.
- Good, because it gives precise visibility into which provider-owned tools are being used.
- Good, because it can allow some tool calls while rejecting others within the same execution.
- Bad, because it interrupts long-running CodeAct flows and can degrade the user experience significantly.
- Bad, because it requires more complex runtime plumbing and approval UX in both SDKs.
- Bad, because repeated approval pauses may make CodeAct less useful for the exact long-running scenarios that motivate this feature.
## Decision Outcomes
### Decision 1: Integration seam and public structure
Chosen option: **Option 1: Standardize on provider-based CodeAct with a shared cross-SDK contract and backend-specific public types**, because it is the only option that maps cleanly to both SDKs, lets us reshape instructions and tools before model invocation, and avoids invasive changes to the existing function invocation loops while still allowing multiple backend-specific providers and multiple runtime modes to fit under the same structure later.
### Decision 2: Initial approval model
Chosen option: **Option A: Bundled approval for the `execute_code` invocation**, because it is the smallest approval model that fits both SDKs, works well for long-running CodeAct flows, and does not force us to standardize a more complex inspection or policy engine in the first release.
This follows the spirit of the current Python tool approval flow, where `FunctionTool` uses `approval_mode="always_require" | "never_require"` and the auto-invocation loop escalates the whole batch when any called tool requires approval.
### Design summary
We standardize the **public concept** of CodeAct across SDKs while allowing each SDK to use the extension point that fits it best.
- Python uses a `ContextProvider`.
- .NET uses an `AIContextProvider`.
- The term **CodeAct context provider** is used throughout this ADR as a design concept, not as a required public base type. Public SDK APIs should prefer concrete backend-specific types such as `HyperlightCodeActProvider` rather than a public abstract `CodeActContextProvider` or a public `CodeActExecutor` parameter.
- CodeAct support should ship as an optional package in each SDK rather than as part of the core package, so users who do not need CodeAct do not take on its installation and dependency footprint. That optional package may still depend on a few small, backward-compatible hooks in the host SDK's core agent pipeline.
- There is no separate runtime setup object in the chosen design. Concrete providers manage their provider-owned CodeAct tool registry, file mounts, and outbound network allow-list configuration directly through CRUD-style methods on the provider itself.
- At a high level, CodeAct is exposed through backend-specific context providers that contribute an `execute_code` tool, own the CodeAct-specific tool registry, and carry backend capability configuration such as filesystem and network access.
- The initial approval model is bundled approval for `execute_code`, using the same `approval_mode="always_require" | "never_require"` vocabulary as regular tools.
- The CodeAct provider exposes a default `approval_mode` for `execute_code`. If the provider default is `always_require`, `execute_code` is always treated as `always_require` regardless of the provider-owned tool registry. If the provider default is `never_require`, the effective approval for `execute_code` is derived from the provider-owned CodeAct tool registry captured for the run.
- If every provider-owned CodeAct tool in that registry has `approval_mode="never_require"`, `execute_code` is treated as `never_require`. If any provider-owned CodeAct tool in that registry has `approval_mode="always_require"`, `execute_code` is treated as `always_require`, even if the generated code may not end up calling that tool.
- Approval is granted before `execute_code` starts, and provider-owned tool calls made from inside that execution run under the same approval.
- Direct-only agent tools do not affect the approval of `execute_code`; only the provider-owned CodeAct tool registry participates in that calculation.
- This approval model is intentionally conservative. If one sensitive provider-owned tool forces `execute_code` to require approval more often than desired, the mitigation is to keep that tool direct-only or split it into a different provider/tool surface rather than trying to infer per-run tool usage up front.
- Configuring filesystem and network capability state on the provider, including adding file mounts or outbound network allow-list entries, is itself the approval for those capabilities in the initial model.
- Each `execute_code` invocation must start from a clean execution state; in-memory variables and other ephemeral interpreter/runtime state must not persist across separate calls. When a provider exposes a workspace, mounted files, or a writable artifact/output area, those files are the supported persistence mechanism across calls and are treated as external state rather than interpreter state.
- Mutating the provider's tool registry or capability configuration while a run is in flight is allowed, but it only affects subsequent runs. Provider implementations must snapshot the effective state for each run and synchronize concurrent access so shared provider instances remain safe across concurrent runs.
- The minimum cross-SDK telemetry contract is that `execute_code` is traced as a normal tool invocation nested inside the surrounding agent run, and provider-owned tool calls made from inside CodeAct continue to emit ordinary tool-invocation telemetry. Backend-specific resource metrics are optional extensions, not a required new top-level cross-SDK event model.
- Timeout, out-of-memory, backend crash, and similar sandbox failures are all execution failures of `execute_code` and should surface as structured error results rather than backend-specific public DTOs. Partial textual or file outputs may be returned only when the backend can report them unambiguously; callers must not rely on partial-output recovery as a portable guarantee.
- The provider-based structure preserves room for future pre-execution inspection and nested per-tool approvals if later experience shows they are needed.
- Concrete backend-specific providers may still use small SDK-local helpers or adapters internally, but that split is an implementation detail rather than a public API requirement.
Detailed language-specific implementation notes are specified in:
- [Python implementation](../features/code_act/python-implementation.md)
- [.NET implementation](../features/code_act/dotnet-implementation.md)
### Minimal core hooks required by the optional package
CodeAct remains optional at the package level, but the optional package depends on a small number of hooks that must live in the host SDK because the agent pipeline owns model invocation and per-run tool resolution.
- Python depends on the existing `ContextProvider` lifecycle, `SessionContext.extend_instructions(...)`, `SessionContext.extend_tools(...)`, per-run runtime tool access via `SessionContext.options["tools"]`, and the shared `ApprovalMode` vocabulary used by `FunctionTool`.
- .NET depends on the existing `AIContextProvider` seam, agent/runtime support for applying providers before model invocation, and the existing chat-client or function-invocation seams that concrete implementations use to contribute `execute_code`.
These hooks are backward-compatible because they only expose or forward per-run state that core already owns. Behavior changes only when a concrete CodeAct provider opts in and uses them.
### Concrete provider implementation contract
The design does not require a public abstract `CodeActContextProvider` base class, but it does require a stable implementation contract for concrete providers.
- Concrete providers should expose a standard capability surface at construction time, with SDK-appropriate naming for:
- approval mode
- workspace root
- file mounts
- allowed outbound targets plus any per-target method or policy restrictions needed by the backend
- Separate public `filesystem_mode` / `network_mode` flags are not required by the cross-SDK contract. Filesystem access may be disabled implicitly until a workspace or file mounts are configured, and outbound network may be disabled implicitly until an allow-list or equivalent outbound policy entry is configured.
- Concrete providers should expose direct CRUD-style methods for managing the provider-owned CodeAct tool registry, file mounts, and outbound network allow-list configuration, rather than requiring callers to construct a separate runtime setup object.
- Concrete providers should implement their host SDK's provider lifecycle hooks to:
- build CodeAct instructions,
- add `execute_code`,
- snapshot the effective CodeAct tool registry and capability settings for the run,
- compute the effective approval requirement for `execute_code`,
- configure file access and network access for the backend,
- prepare or restore execution state,
- execute code,
- and translate backend output into framework-native content.
- Any internal abstract/helper surface shared by multiple concrete providers should standardize responsibilities for:
- instruction construction,
- file-access configuration,
- network-access configuration,
- environment preparation/restoration,
- code execution,
- and output-to-content conversion.
- Backend execution output should reuse existing framework-native content/message primitives rather than introducing backend-specific public result DTOs.
## More Information
### Related artifacts
- Python implementation: [`docs/features/code_act/python-implementation.md`](../features/code_act/python-implementation.md)
- .NET implementation: [`docs/features/code_act/dotnet-implementation.md`](../features/code_act/dotnet-implementation.md)
- Python provider/session APIs: [`python/packages/core/agent_framework/_sessions.py`](../../python/packages/core/agent_framework/_sessions.py)
- Python function invocation loop: [`python/packages/core/agent_framework/_tools.py`](../../python/packages/core/agent_framework/_tools.py)
- .NET context provider abstraction: [`dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs`](../../dotnet/src/Microsoft.Agents.AI.Abstractions/AIContextProvider.cs)
- .NET agent integration for context providers: [`dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs`](../../dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs)
- Optional .NET chat-client provider decorator: [`dotnet/src/Microsoft.Agents.AI/AIContextProviderDecorators/AIContextProviderChatClient.cs`](../../dotnet/src/Microsoft.Agents.AI/AIContextProviderDecorators/AIContextProviderChatClient.cs)
- .NET function invocation middleware seam: [`dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgentBuilderExtensions.cs`](../../dotnet/src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgentBuilderExtensions.cs)
### Related decisions
- [0015-agent-run-context](0015-agent-run-context.md)
- [0016-python-context-middleware](0016-python-context-middleware.md)
@@ -0,0 +1,454 @@
---
status: proposed
contact: evmattso
date: 2026-04-10
deciders: evmattso
---
# Foundry Toolbox Support in FoundryChatClient
## What is the goal of this feature?
Enable Agent Framework users to consume Foundry **toolboxes** — named, versioned bundles of tool definitions stored server-side in an Azure AI Foundry project — directly from `FoundryChatClient`, without dropping to the raw `azure-ai-projects` SDK.
A user who has configured a toolbox in the Foundry portal (or via the raw SDK) should be able to load it into an agent with a single call:
```python
toolbox = await client.get_toolbox("research_tools")
agent = Agent(client=client, instructions="...", tools=toolbox)
```
**Success metric:** an agent can consume a toolbox with no manual handling of version-resolution logic on the user's side.
## What is the problem being solved?
`azure-ai-projects==2.1.0a20260409002` ships a new `BetaToolboxesOperations` surface, reachable as `AIProjectClient.beta.toolboxes` on the raw SDK client (and therefore as `FoundryChatClient.project_client.beta.toolboxes` through our wrapper), that lets teams:
- Group related hosted tools (code interpreter, file search, MCP, web search, etc.) under a named toolbox
- Version toolboxes immutably, so agents can pin to a specific configuration for production stability
- Share toolboxes across multiple agents in a project
However, consuming a toolbox from the framework today requires:
1. Knowing the raw SDK accessor path (`client.project_client.beta.toolboxes`)
2. Making two calls for the common case — `.get(name)` to find the default version, then `.get_version(name, version)` to actually retrieve tools
3. Manually unpacking `toolbox.tools` before passing them to `Agent(tools=...)`
None of this is hard, but it's the kind of boilerplate that should live in the client. Every other hosted tool in `FoundryChatClient` (code interpreter, file search, web search, image generation, MCP) already has a factory method (`get_code_interpreter_tool()`, etc.). Toolbox support should fit the same shape on the chat-client composition surface.
## API Changes
### One new method on the FoundryChatClient surface
The public toolbox-consumption surface lands on:
- `RawFoundryChatClient` (inherited by `FoundryChatClient`) in `_chat_client.py`
The implementation delegates to shared helper functions in `_tools.py` so there is a single source of truth for the SDK calls.
**Scope note:** `FoundryAgent` is intentionally not part of this design. `FoundryAgent` is the runtime surface for invoking an already-configured server-side Foundry agent; if that agent should use a toolbox, the toolbox/tools should already be configured on the Foundry side (UI or `azure-ai-projects` authoring flow) before MAF connects to it.
**Scope note:** Authoring a server-side agent whose definition references a toolbox (via `PromptAgentDefinition(tools=toolbox.tools, ...)` + `client.agents.create_version(...)`) is deliberately outside MAF scope. That is an `azure-ai-projects` / service-resource authoring concern, not a future MAF feature. Users who need it should use the raw Azure SDK directly.
```python
async def get_toolbox(
self,
name: str,
*,
version: str | None = None,
) -> ToolboxVersionObject:
"""Fetch a Foundry toolbox by name.
If ``version`` is ``None``, resolves the toolbox's current default version
(two requests). If ``version`` is specified, fetches that version directly
(single request).
:param name: The name of the toolbox.
:param version: Optional immutable version identifier to pin to.
:return: A ``ToolboxVersionObject``. Pass its ``tools`` attribute to
``Agent(tools=toolbox.tools)``.
:raises azure.core.exceptions.ResourceNotFoundError: If the toolbox or
version does not exist.
"""
```
### Return types: raw SDK models, no custom wrappers
Methods return the `azure.ai.projects.models` types directly:
- `get_toolbox()``ToolboxVersionObject` (has `.name`, `.version`, `.tools`, `.id`, `.created_at`, `.description`, `.metadata`, `.policies`)
No custom wrapper classes are defined. Returning the SDK types directly:
- Eliminates maintenance overhead of keeping a custom wrapper aligned with SDK changes
- Matches the existing convention — `get_code_interpreter_tool()` returns the raw `CodeInterpreterTool` SDK type
- Means any new fields the SDK adds to these types flow through automatically
`Agent(..., tools=...)` will accept the fetched toolbox object directly by flattening to `toolbox.tools` internally.
### Design decisions
**Instance methods, not `@staticmethod` factories.** Existing `get_code_interpreter_tool()` / `get_mcp_tool()` / etc. are `@staticmethod` because they're pure factories with no network I/O. Toolbox fetching requires the project client, so these new methods must be instance methods. This is a deliberate departure from the existing-factory pattern, justified by the async-with-I/O nature of the operation.
**Raw SDK type passthrough (no custom wrappers).** There is only one toolbox type in the Foundry SDK and maintaining a shadow wrapper would create alignment risk as the SDK evolves. The raw `ToolboxVersionObject` and `ToolboxObject` carry all the fields users need. Individual tools inside `toolbox.tools` are the same `azure.ai.projects.models.Tool` subclasses returned by other factory methods.
**Two-request default-version path.** When `version=None`, implementation calls `.get(name)` to find `default_version`, then `.get_version(name, default_version)` for the tools. Caching the default-version mapping was considered and rejected — default versions can change server-side via `update(default_version=...)`, and a stale cache would silently give callers the wrong tools. Two requests at agent setup is acceptable.
**No discovery/listing surface in MAF.** Discovery is intentionally left to the raw `azure-ai-projects` client. MAF does not currently expose project-resource listing surfaces for many other Foundry resources (deployments, vector stores, agents, etc.), so the toolbox design stays narrowly focused on explicit retrieval by name/version.
**Shared helpers in `_tools.py`.** The SDK-call helper function (`fetch_toolbox`) lives in a shared module so the chat-client surface stays thin and the request logic remains centralized.
**`tools=toolbox` convenience, not a new wrapper type.** Although `get_toolbox()` returns the raw `ToolboxVersionObject`, Agent Framework can still support `tools=toolbox` / `tools=[toolbox]` by flattening the toolbox's `.tools` internally. That matches existing SDK ergonomics where some higher-level objects can be placed directly in `tools=` and unpacked underneath, without introducing a public `FoundryToolbox` wrapper.
**Errors pass through unchanged.** `ResourceNotFoundError`, `HttpResponseError`, etc. from the SDK propagate as-is. No framework-specific exception hierarchy.
## E2E Code Samples
### Primary sample
New file: `samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox.py`
```python
import asyncio
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
async def main() -> None:
client = FoundryChatClient(credential=AzureCliCredential())
toolbox = await client.get_toolbox("research_tools")
print(f"Loaded toolbox {toolbox.name}@{toolbox.version} ({len(toolbox.tools)} tools)")
agent = Agent(
client=client,
instructions="You are a research assistant.",
tools=toolbox,
)
result = await agent.run("What are the latest developments in quantum error correction?")
print(f"Result: {result}")
if __name__ == "__main__":
asyncio.run(main())
```
### Version pinning
```python
toolbox = await client.get_toolbox("research_tools", version="v3")
```
### Combining multiple toolboxes
```python
toolbox_a = await client.get_toolbox("research_tools")
toolbox_b = await client.get_toolbox("some_other_tools", version="v3")
agent = Agent(
client=client,
instructions="...",
tools=[toolbox_a, toolbox_b],
)
```
### Combining toolbox tools with locally defined tools
```python
toolbox = await client.get_toolbox("research_tools")
def get_internal_metrics(metric_name: str) -> dict:
"""Custom tool that reads from an internal dashboard."""
...
agent = Agent(
client=client,
instructions="...",
tools=[get_internal_metrics, toolbox],
)
```
### Selecting only some tools from a toolbox
Developers will not always want to pass the entire toolbox through unchanged. A
small helper in the Foundry package provides local post-fetch selection without
changing the raw return type of `get_toolbox()`.
```python
from agent_framework.foundry import select_toolbox_tools
toolbox = await client.get_toolbox("research_tools")
selected_tools = select_toolbox_tools(
toolbox,
include_names=["githubmcp", "code_interpreter"],
)
agent = Agent(
client=client,
instructions="Use only the selected toolbox tools.",
tools=selected_tools,
)
```
Supported filters:
```python
from agent_framework.foundry import FoundryHostedToolType, select_toolbox_tools
selected_tools = select_toolbox_tools(
toolbox,
include_types=["mcp", "code_interpreter"], # type: Collection[FoundryHostedToolType]
exclude_names=["internal_admin_tool"],
)
```
Helper signature:
```python
type FoundryHostedToolType = Literal[
"code_interpreter",
"file_search",
"image_generation",
"mcp",
"web_search",
] | str
def select_toolbox_tools(
tools: ToolboxVersionObject | Sequence[Tool | dict[str, Any]],
*,
include_names: Collection[str] | None = None,
exclude_names: Collection[str] | None = None,
include_types: Collection[FoundryHostedToolType] | None = None,
exclude_types: Collection[FoundryHostedToolType] | None = None,
predicate: Callable[[Tool | dict[str, Any]], bool] | None = None,
) -> list[Tool | dict[str, Any]]:
...
```
Normalized name precedence for `include_names` / `exclude_names`:
1. MCP `server_label`
2. generic tool `name`
3. fallback tool `type`
This keeps `get_toolbox()` as a thin fetch API and makes selection an explicit,
local post-processing step, while still allowing the ergonomic
`select_toolbox_tools(toolbox, ...)` call shape.
## Native vs MCP consumption of a Foundry toolbox
A Foundry toolbox can be consumed two ways. This design adds new implementation work only for the first:
1. **Native consumption (in scope).** Tools execute inside Foundry's agent runtime. `get_toolbox()` returns the `ToolboxVersionObject` whose `.tools` attribute carries typed tool configs that the runtime interprets server-side. This design is specifically for `FoundryChatClient`-backed local agent composition.
2. **MCP consumption (already supported through existing MCP abstractions).** A Foundry toolbox can also be exposed as an MCP server. In that case, use the existing `MCPStreamableHTTPTool(name=..., url=...)` — it already handles this path with any chat client (Foundry, OpenAI, Anthropic, etc.). No new Foundry-specific API is needed for MCP-exposed toolboxes in this design.
### MCPStreamableHTTPTool example for a Foundry toolbox endpoint
If Foundry gives you an MCP endpoint for the toolbox (for example from the
toolbox details UI / endpoint surface), the existing MCP client path is:
```python
from agent_framework import Agent, MCPStreamableHTTPTool
from agent_framework.openai import OpenAIChatClient
toolbox_mcp = MCPStreamableHTTPTool(
name="research_tools",
url="https://<foundry-toolbox-mcp-endpoint>",
)
agent = Agent(
client=OpenAIChatClient(),
instructions="You are a research assistant.",
tools=[toolbox_mcp],
)
```
This is a different integration shape than `get_toolbox(...).tools`:
- `get_toolbox(...).tools` = **native Foundry hosted-tool configs** interpreted by the
Foundry runtime
- `MCPStreamableHTTPTool(name=..., url=...)` = **live MCP server connection** to a
toolbox endpoint
The design in this spec adds first-class support only for the native hosted-tool
path. The MCP path is already served by the framework's existing MCP abstractions.
These paths are not unified because they have fundamentally different execution models. Native toolbox tools are declarative configs the Foundry runtime executes; MCP consumption is a live wire protocol to a running server.
**MCP authentication inside a toolbox** is handled server-side via `project_connection_id` on individual `MCPTool` entries (OAuth connection objects configured in the Foundry project). The client never holds bearer tokens. Consent flow handling (`CONSENT_REQUIRED` → user-visible consent URL) happens during `agent.run()`, not during toolbox fetching — see Non-goals.
## Testing Strategy
Unit tests in `packages/foundry/tests/test_toolbox.py` with mocked `project_client.beta.toolboxes`. A single opt-in live round-trip, `test_integration_get_toolbox_round_trip_against_real_project`, is marked `@pytest.mark.integration`; it is skipped by default and only runs when the required Foundry credentials are available.
Coverage:
- `get_toolbox(name, version="v3")` — explicit version, single request. Assert `.get` not called, `.get_version` awaited once, returns `ToolboxVersionObject`.
- `get_toolbox(name)` — default-version resolution. Assert `.get` then `.get_version` called in order with correct args.
- Error propagation — `ResourceNotFoundError` from `.get` propagates unchanged.
- Tool passthrough — heterogeneous tool list (`CodeInterpreterTool`, `MCPTool(project_connection_id=...)`) passes through unchanged. Asserts `project_connection_id` survives.
- Agent integration smoke — `tools=toolbox` / `tools=[toolbox]` flatten to the underlying toolbox tools.
- Multiple toolbox composition smoke — `tools=[toolbox_a, toolbox_b]` flattens into a single agent tool list.
- `get_toolbox_tool_name()` — selection-name precedence is MCP `server_label`, then `name`, then `type`.
- `select_toolbox_tools(toolbox, include_names=...)` — selects by normalized tool names directly from a fetched toolbox object.
- `select_toolbox_tools(toolbox, include_types=...)` — selects by tool types with `Literal`-guided IDE completion.
- `select_toolbox_tools(..., exclude_names=..., predicate=...)` — supports exclusion + custom predicates.
Deliberately **not** covered:
- Runtime consent-flow handling for OAuth MCP tools (see Non-goals).
- Toolbox discovery/listing (`list_toolboxes`, `list_toolbox_versions`) — deliberately left to the raw Azure SDK.
- Full CRUD (`create_version`, `update`, `delete`) and server-side agent authoring — see Non-goals.
Live Foundry API integration is exercised only through the opt-in `@pytest.mark.integration` round-trip noted above; it is not part of the default test run.
## Framework dependency: `normalize_tools` flattening
The core `normalize_tools` function in `packages/core/agent_framework/_tools.py` already supports flattening composite tool inputs. Toolbox support extends that behavior so a fetched `ToolboxVersionObject` is treated as a composite tool source and flattened to its `.tools`.
That enables:
- `tools=toolbox`
- `tools=[toolbox]`
- `tools=[local_tool, toolbox]`
- `tools=[toolbox_a, toolbox_b]`
while still keeping `select_toolbox_tools(toolbox.tools, ...)` available for partial selection before the final agent construction step.
## Telemetry
Telemetry for toolbox support has two separate goals:
1. **Observe toolbox API access**`get_toolbox()`
2. **Observe toolbox usage during agent runs** — when users pass toolbox-derived tools into `Agent(..., tools=...)`
### Request telemetry for toolbox API access
When Agent Framework constructs the `AIProjectClient` internally for `FoundryChatClient`, it already sets:
```python
user_agent=AGENT_FRAMEWORK_USER_AGENT
```
That means toolbox API requests made through:
- `project_client.beta.toolboxes.get(...)`
- `project_client.beta.toolboxes.get_version(...)`
carry the standard MAF user-agent marker and can be queried in backend request logs the same way as other Foundry SDK calls made through framework-owned clients.
Important constraint: if the caller passes an already-constructed `project_client`, Agent Framework does **not** mutate it to inject the MAF user-agent. In that case, toolbox API request telemetry reflects whatever user-agent behavior that external client was configured with.
### Runtime telemetry for toolbox usage on agent runs
Tool-level telemetry already captures which hosted Foundry tools are available / invoked during agent execution. The remaining gap is **toolbox provenance**: once the user writes `tools=toolbox` (or otherwise flattens the toolbox into tool configs), the framework sees only raw tool configs and no longer knows which toolbox name/version supplied them.
The design for closing the **client-side** observability gap is **internal provenance tracking**, not user-supplied metadata and not a new public wrapper type.
#### Provenance model
Note: this section is still under investigation.
When `get_toolbox()` or `list_toolbox_versions()` returns a `ToolboxVersionObject`, Agent Framework will attach private provenance metadata to:
- the returned toolbox object
- each tool inside `toolbox.tools`
Recommended shape (private, internal-only):
```python
tool._maf_toolbox_sources = [
{
"id": toolbox.id,
"name": toolbox.name,
"version": toolbox.version,
}
]
```
Key properties of this approach:
- **No new public API surface** — users still work with raw `ToolboxVersionObject` / `ToolboxObject`
- **No user burden** — callers do not need to stamp metadata manually
- **Provenance follows the tool objects** — works with:
- `tools=toolbox.tools`
- `tools=[toolbox_a.tools, toolbox_b.tools]`
- `tools=[*toolbox_a.tools, *toolbox_b.tools]`
- **Private attributes are not serialized** into the actual request payload sent to the model/service, so this metadata does not leak into the tool definition body
This is intentionally preferred over introducing a new public `FoundryToolbox` wrapper purely for telemetry, and preferred over a separate global provenance registry. The provenance lives on the existing tool objects so list-copying and chat-option merging naturally preserve it.
#### Span enrichment
When Agent / chat telemetry computes span attributes for a run, it should inspect the final tool list and aggregate the private toolbox provenance from any tool objects that carry it. The aggregated values are then emitted as attributes on the existing run/chat spans.
Suggested custom attributes:
- `agent_framework.foundry.toolbox.ids`
- `agent_framework.foundry.toolbox.names`
- `agent_framework.foundry.toolbox.versions`
- or a single compact attribute such as `agent_framework.foundry.toolbox.sources=["research_tools@1","some_other_tools@3"]`
The single compact `toolbox.sources` form is preferred for initial implementation because it is easy to query and easy to render from combined tool lists.
#### Scope of telemetry changes
This design does **not** require new spans. It enriches existing telemetry:
- toolbox API access continues to rely on request logs + Azure SDK distributed tracing + MAF user-agent
- agent/chat execution spans gain toolbox provenance attributes when toolbox-derived tools are present
Implementation-wise, this design most likely touches:
- `packages/foundry/agent_framework_foundry/_tools.py` — to stamp provenance on fetched toolbox objects / tools
- `packages/core/agent_framework/observability.py` — to aggregate provenance into span attributes
#### Important limitation: no server-side toolbox telemetry solution yet
Private provenance attached to tool objects is only useful on the client side. It
does **not** go over the wire to the Foundry service because those private fields
are intentionally not serialized into the request payload.
That means this design can support:
- local OpenTelemetry / exporter spans emitted by Agent Framework
- local attribution of a run to one or more fetched toolboxes
but it does **not** solve:
- server-side request-log attribution of a model/tool run back to a toolbox
- backend/database queries that need the service itself to know "this tool came from toolbox X"
At the moment, we do not have a satisfactory design for server-side toolbox
telemetry. The service would require additional structured information on the
request, and there is no accepted mechanism in this design yet for projecting
toolbox provenance into a server-visible field/header/metadata shape.
So the telemetry story in this spec is explicitly limited to **client-side
toolbox telemetry**. Server-side toolbox attribution remains an open question and
requires either:
- new service/API support, or
- a later framework design for emitting additional server-visible request metadata.
#### Deliberate non-goals for telemetry
- No requirement for users to pass explicit toolbox metadata in `default_options["metadata"]` or `run(..., options=...)`
- No new public `FoundryToolbox` wrapper type just to preserve attribution
- No attempted server-side attribution mechanism in this design (for example a custom request header or request metadata field) until there is a validated end-to-end contract for it
## Non-goals / Future Work
Explicitly out of scope for this design. Each is a separate design and PR when needed.
1. **Create/update/delete toolboxes from code.** CRUD is rare in agent consumption flows. Users who need it drop to `client.project_client.beta.toolboxes.create_version(...)`, `.update(...)`, `.delete(...)` directly.
2. **Server-side agent authoring from toolbox.** Creating a `PromptAgentDefinition(tools=toolbox.tools)` + `client.agents.create_version(...)` is a future feature covering agent authoring from code. The toolbox read API provides the building blocks; the authoring helpers are a separate design.
3. **OAuth consent-flow runtime handling.** When a toolbox contains MCP tools with `project_connection_id` pointing to an OAuth connection, the runtime may return `CONSENT_REQUIRED` mid-run. This is a runtime concern separate from toolbox fetching.
4. **Live integration tests.** This PR ships unit tests only.
5. **Toolbox caching or refresh APIs.** Each `get_toolbox()` call hits the network. Users who want caching wrap the call themselves.
@@ -0,0 +1,625 @@
# CodeAct .NET implementation
This document describes the .NET realization of the CodeAct design in
[`docs/decisions/0024-codeact-integration.md`](../../decisions/0024-codeact-integration.md).
This document is intentionally focused on the .NET design and public API surface.
The initial public .NET type described here is `HyperlightCodeActProvider`. Future .NET backends, such as Monty, should follow the same conceptual model with their own concrete provider types rather than through a public abstract base class or a public executor parameter.
## What is the goal of this feature?
Goals:
- .NET developers can enable CodeAct through an `AIContextProvider`-based integration.
- Developers can configure a provider-owned CodeAct tool set that is separate from the agent's direct tool surface.
- Developers can use the same `execute_code` concept for both tool-enabled CodeAct and a standard code interpreter tool implementation.
- Developers can swap execution backends over time, starting with Hyperlight while keeping room for alternatives.
- Developers can configure execution capabilities such as workspace mounts and outbound network allow lists in a portable way.
Success Metric:
- .NET samples exist for both a tool-enabled CodeAct mode and a standard interpreter mode.
Implementation-free outcome:
- A .NET developer can attach a backend-specific CodeAct provider, choose which tools are available inside CodeAct, and configure execution capabilities without rewriting the function invocation loop or ChatClient pipeline.
## What is the problem being solved?
The cross-SDK problem statement and decision rationale live in the [ADR](../../decisions/0024-codeact-integration.md). The items below narrow that statement to .NET-specific design concerns:
- Today, the easiest way to prototype CodeAct in .NET is to manually configure an `AIFunction` and wire instructions — this is fragile and requires understanding internal sandbox lifecycle details.
- There is no first-class .NET design that simultaneously covers Hyperlight-backed CodeAct now, future backend-specific providers, and both tool-enabled and interpreter modes.
- Sandbox capabilities such as mounted file access and outbound network access need a portable configuration model instead of ad hoc backend-specific wiring.
- Approval behavior needs to be explicit and configurable, mapping to .NET's existing `ApprovalRequiredAIFunction` wrapper mechanism.
## API Changes
### CodeAct contract
#### Terminology
- **CodeAct** is the primary term.
- `execute_code` is the model-facing tool name used by the initial .NET provider in this spec.
- Tool-enabled versus interpreter behavior is derived from the presence of CodeAct-managed tools, not from a separate public profile object.
#### Provider-owned CodeAct tool registry
A concrete .NET CodeAct provider owns the set of tools available through `call_tool(...)` inside CodeAct.
Rules:
- Only tools explicitly configured on the concrete provider instance are available inside CodeAct.
- The provider must not infer its CodeAct-managed tool set from the agent's direct tool configuration (`ChatClientAgentOptions.Tools` or `AIContext.Tools`).
- Exclusive versus mixed behavior is achieved by where tools are configured, not by rewriting the agent's direct tool list.
Implications:
- **CodeAct-only tool**: configured on the concrete CodeAct provider only.
- **Direct-only tool**: configured on the agent only.
- **Tool available both ways**: configured on both the agent and the concrete CodeAct provider.
#### Managing tools and capabilities after provider construction
There is no separate runtime setup object in the .NET design. CodeAct tools, file mounts, and outbound network allow-list state are managed directly on the provider through CRUD-style registry methods.
Preferred pattern:
- `AddTools(params AIFunction[] tools) -> void`
- `GetTools() -> IReadOnlyList<AIFunction>`
- `RemoveTools(params string[] names) -> void`
- `ClearTools() -> void`
- `AddFileMounts(params FileMount[] mounts) -> void`
- `GetFileMounts() -> IReadOnlyList<FileMount>`
- `RemoveFileMounts(params string[] mountPaths) -> void`
- `ClearFileMounts() -> void`
- `AddAllowedDomains(params AllowedDomain[] domains) -> void`
- `GetAllowedDomains() -> IReadOnlyList<AllowedDomain>`
- `RemoveAllowedDomains(params string[] targets) -> void`
- `ClearAllowedDomains() -> void`
Requirements:
- The provider-owned CodeAct tool registry is keyed by tool name (from `AIFunction.Name`).
- `AddTools(...)` adds new tools and replaces an existing provider-owned registration when the same tool name is added again.
- `GetTools()` returns the provider's current configured CodeAct tool registry.
- `RemoveTools(...)` removes provider-owned CodeAct tools by name.
- `ClearTools()` removes all provider-owned CodeAct tools.
- File mounts are keyed by sandbox mount path.
- `AddFileMounts(...)` adds new file mounts and replaces an existing mount when the same mount path is added again.
- `GetFileMounts()` returns the provider's current configured file mounts.
- `RemoveFileMounts(...)` removes file mounts by mount path.
- `ClearFileMounts()` removes all configured file mounts.
- Allowed domains are keyed by normalized target string.
- `AddAllowedDomains(...)` adds allow-list entries and replaces an existing entry when the same target is added again.
- `GetAllowedDomains()` returns the current outbound allow-list entries.
- `RemoveAllowedDomains(...)` removes allow-list entries by target.
- `ClearAllowedDomains()` removes all configured allow-list entries.
- Tool, file-mount, and network-allow-list mutations affect subsequent runs only; runs already in progress keep the snapshot captured at run start.
- The provider must snapshot its effective tool registry and capability state at the start of each run so concurrent execution remains deterministic.
#### Approval model
The initial .NET design follows the ADR's bundled approval decision and maps to the existing `ApprovalRequiredAIFunction` wrapper from `Microsoft.Extensions.AI.Abstractions`:
- The provider exposes a default `ApprovalMode` for `execute_code` (enum: `CodeActApprovalMode.AlwaysRequire` / `CodeActApprovalMode.NeverRequire`).
Effective `execute_code` approval is computed as follows:
- If the provider default is `AlwaysRequire`, `execute_code` requires approval.
- If the provider default is `NeverRequire`, the provider evaluates the provider-owned CodeAct tool registry snapshot for that run.
- If every provider-owned CodeAct tool in that snapshot is not an `ApprovalRequiredAIFunction`, `execute_code` does not require approval.
- If any provider-owned CodeAct tool in that snapshot is an `ApprovalRequiredAIFunction`, `execute_code` requires approval, even if the generated code may not call that tool.
- When the effective approval resolves to `AlwaysRequire`, the generated `execute_code` function is wrapped in `ApprovalRequiredAIFunction` before being added to the `AIContext.Tools`.
- Provider-owned tool calls made through `call_tool(...)` during that execution run use the approval already determined for `execute_code`.
- Direct-only agent tools are excluded from this calculation.
- File and network capabilities do not create a separate runtime approval check in the initial model; configuring them on the provider is itself the approval for those capabilities.
This is intentionally conservative and matches the shape of the existing .NET function-tool approval flow, where `ApprovalRequiredAIFunction` signals to the `ChatClientAgent` that user approval is needed before invocation.
#### Shared execution flow
On each run:
1. `ProvideAIContextAsync(...)` snapshots the current CodeAct-managed tool registry and capability settings.
2. Computes the effective approval requirement for `execute_code` from the provider default plus the snapshotted tool registry.
3. Builds provider-defined instructions.
4. Builds a run-scoped `execute_code` `AIFunction` from the snapshot (optionally wrapped in `ApprovalRequiredAIFunction`).
5. Returns an `AIContext` containing the instructions and `execute_code` tool.
6. When `execute_code` is invoked by the model, the run-scoped function creates or reuses an execution environment.
7. If the current provider mode exposes host tools, `call_tool(...)` is bound only to the provider-owned tool registry snapshot.
8. Code is executed and results converted to a JSON result string.
Caching rules:
- The Hyperlight backend supports snapshots: the provider caches a reusable clean snapshot after the first sandbox initialization.
- No mutable per-run execution state may be shared across concurrent runs.
- In-memory interpreter state does not persist across separate `execute_code` calls.
- Configured workspace files, mounted files, and any writable artifact/output area are the supported persistence mechanism across calls when the backend exposes them.
### .NET public API
#### Core types
```csharp
/// <summary>
/// Represents a host-to-sandbox file mount configuration.
/// </summary>
/// <param name="HostPath">Absolute or relative path on the host filesystem.</param>
/// <param name="MountPath">Path inside the sandbox (e.g. "/input/data.csv").</param>
public sealed record FileMount(string HostPath, string MountPath);
/// <summary>
/// Represents an outbound network allow-list entry.
/// </summary>
/// <param name="Target">URL or domain (e.g. "https://api.github.com").</param>
/// <param name="Methods">
/// Optional HTTP methods to allow (e.g. ["GET", "POST"]).
/// Null allows all methods supported by the backend.
/// </param>
public sealed record AllowedDomain(string Target, IReadOnlyList<string>? Methods = null);
/// <summary>
/// Controls the approval behavior for execute_code invocations.
/// </summary>
public enum CodeActApprovalMode
{
/// <summary>execute_code always requires user approval.</summary>
AlwaysRequire,
/// <summary>
/// Approval is derived from the provider-owned tool registry:
/// if any tool is an ApprovalRequiredAIFunction, execute_code requires approval.
/// </summary>
NeverRequire,
}
```
#### HyperlightCodeActProvider
```csharp
/// <summary>
/// An AIContextProvider that enables CodeAct execution through the
/// Hyperlight sandbox backend.
/// </summary>
/// <remarks>
/// <para>
/// This provider injects an <c>execute_code</c> tool into the model-facing
/// tool surface and builds CodeAct guidance instructions. Guest code executed
/// through <c>execute_code</c> runs in an isolated Hyperlight sandbox with
/// snapshot/restore for clean state per invocation.
/// </para>
/// <para>
/// If no CodeAct-managed tools are configured, the provider uses
/// interpreter-style behavior. If one or more CodeAct-managed tools are
/// configured, the provider uses tool-enabled behavior and exposes
/// <c>call_tool(...)</c> inside the sandbox bound to the configured tools.
/// </para>
/// </remarks>
public sealed class HyperlightCodeActProvider : AIContextProvider, IDisposable
{
/// <summary>
/// Initializes a new HyperlightCodeActProvider.
/// </summary>
/// <param name="options">Configuration options for the provider.</param>
public HyperlightCodeActProvider(HyperlightCodeActProviderOptions options);
// ----- Tool registry -----
/// <summary>Adds tools to the provider-owned CodeAct tool registry.</summary>
public void AddTools(params AIFunction[] tools);
/// <summary>Returns the current CodeAct-managed tools.</summary>
public IReadOnlyList<AIFunction> GetTools();
/// <summary>Removes tools by name from the CodeAct tool registry.</summary>
public void RemoveTools(params string[] names);
/// <summary>Removes all CodeAct-managed tools.</summary>
public void ClearTools();
// ----- File mounts -----
/// <summary>Adds file mount configurations.</summary>
public void AddFileMounts(params FileMount[] mounts);
/// <summary>Returns the current file mount configurations.</summary>
public IReadOnlyList<FileMount> GetFileMounts();
/// <summary>Removes file mounts by sandbox mount path.</summary>
public void RemoveFileMounts(params string[] mountPaths);
/// <summary>Removes all file mount configurations.</summary>
public void ClearFileMounts();
// ----- Network allow-list -----
/// <summary>Adds outbound network allow-list entries.</summary>
public void AddAllowedDomains(params AllowedDomain[] domains);
/// <summary>Returns the current outbound allow-list entries.</summary>
public IReadOnlyList<AllowedDomain> GetAllowedDomains();
/// <summary>Removes allow-list entries by target.</summary>
public void RemoveAllowedDomains(params string[] targets);
/// <summary>Removes all outbound allow-list entries.</summary>
public void ClearAllowedDomains();
// ----- Lifecycle -----
/// <summary>Releases the sandbox and all associated native resources.</summary>
public void Dispose();
}
```
#### HyperlightCodeActProviderOptions
```csharp
/// <summary>
/// Configuration options for <see cref="HyperlightCodeActProvider"/>.
/// </summary>
public sealed class HyperlightCodeActProviderOptions
{
/// <summary>
/// The sandbox backend to use. Default is <c>Wasm</c>.
/// </summary>
public SandboxBackend Backend { get; set; } = SandboxBackend.Wasm;
/// <summary>
/// Path to the guest module (.wasm or .aot file).
/// Required for the Wasm backend; not needed for JavaScript.
/// When null, the provider attempts to locate the default packaged
/// Python guest module.
/// </summary>
public string? ModulePath { get; set; }
/// <summary>
/// Guest heap size. Accepts human-readable strings ("50Mi", "2Gi")
/// or raw byte values. Null uses the backend default.
/// </summary>
public string? HeapSize { get; set; }
/// <summary>
/// Guest stack size. Accepts human-readable strings ("35Mi")
/// or raw byte values. Null uses the backend default.
/// </summary>
public string? StackSize { get; set; }
/// <summary>
/// Initial set of CodeAct-managed tools available inside the sandbox.
/// </summary>
public IEnumerable<AIFunction>? Tools { get; set; }
/// <summary>
/// Default approval mode for the execute_code tool.
/// Default is <see cref="CodeActApprovalMode.NeverRequire"/>.
/// </summary>
public CodeActApprovalMode ApprovalMode { get; set; } = CodeActApprovalMode.NeverRequire;
/// <summary>
/// Optional workspace root directory on the host.
/// When set, it is exposed as the sandbox's input directory.
/// </summary>
public string? WorkspaceRoot { get; set; }
/// <summary>
/// Initial file mount configurations.
/// </summary>
public IEnumerable<FileMount>? FileMounts { get; set; }
/// <summary>
/// Initial outbound network allow-list entries.
/// </summary>
public IEnumerable<AllowedDomain>? AllowedDomains { get; set; }
/// <summary>
/// State key used to store provider state in AgentSession.StateBag.
/// Defaults to "HyperlightCodeActProvider". Override when using
/// multiple provider instances on the same agent.
/// </summary>
public string? StateKey { get; set; }
}
```
#### Provider implementation contract
The concrete provider plugs into the existing .NET `AIContextProvider` surface from `Microsoft.Agents.AI.Abstractions`.
Required override:
- `ProvideAIContextAsync(InvokingContext, CancellationToken) -> ValueTask<AIContext>`
`ProvideAIContextAsync(...)` is responsible for:
- snapshotting the current CodeAct-managed tool registry and capability settings for the run,
- computing the effective approval requirement for `execute_code` from the provider default and the snapshotted tool registry,
- building a short CodeAct guidance instruction string,
- building a run-scoped `execute_code` `AIFunction` from the snapshot,
- optionally wrapping it in `ApprovalRequiredAIFunction` when approval is required,
- and returning an `AIContext` with `Instructions` and `Tools` set.
These steps run on every invocation rather than once at construction time because the provider supports CRUD mutations between runs, concurrent runs need independent snapshots, and the effective approval and instructions depend on the tool registry state captured at run start.
The provider overrides `StateKeys` to return the configured `StateKey` from options, enabling multiple provider instances on the same agent without key collisions.
Mutating the provider after `ProvideAIContextAsync(...)` has captured a run-scoped snapshot is allowed, but it affects subsequent runs only. Provider implementations synchronize state capture and CRUD operations so shared provider instances remain safe across concurrent runs.
#### AIFunction-to-sandbox tool bridging
The Hyperlight sandbox's `RegisterTool(name, Func<string, string>)` accepts a synchronous JSON-in / JSON-out delegate. Provider-owned CodeAct tools are `AIFunction` instances that are async and cancellation-aware.
Bridging strategy:
- At sandbox initialization time, the provider registers each CodeAct-managed tool with the sandbox using the raw JSON overload: `RegisterTool(name, Func<string, string>)`.
- When the sandbox guest calls `call_tool("name", ...)`, the bridge delegate:
1. Deserializes the JSON arguments.
2. Invokes `AIFunction.InvokeAsync(...)` synchronously (via `GetAwaiter().GetResult()`) since the sandbox FFI callback is inherently synchronous.
3. Serializes the result back to JSON.
- This sync-over-async bridge is a known pragmatic trade-off constrained by the Hyperlight FFI boundary. It is safe because:
- Sandbox execution already runs on the thread pool (via `Task.Run`).
- The FFI callback runs on a worker thread with no synchronization context.
- If the Hyperlight .NET SDK later adds async tool registration, the bridge should migrate to that.
#### Runtime behavior
- `ProvideAIContextAsync(...)` adds a short CodeAct guidance block through `AIContext.Instructions`.
- `ProvideAIContextAsync(...)` adds `execute_code` through `AIContext.Tools`.
- The detailed `call_tool(...)`, sandbox-tool, and capability guidance is carried by the `execute_code` function's `Description`.
- `execute_code` invokes the configured Hyperlight sandbox guest.
- If the current CodeAct tool registry snapshot is non-empty, the runtime injects `call_tool(...)` bound to the provider-owned tool registry.
- The provider does not inspect or mutate the agent's `ChatClientAgentOptions.Tools` or the incoming `AIContext.Tools` to determine its CodeAct tool set.
- The provider snapshots the current CodeAct tool registry and capability state at run start, so later registry and allow-list mutations only affect future runs.
- Interpreter versus tool-enabled behavior is derived from the presence of CodeAct-managed tools.
- `execute_code` is traced like a normal tool invocation within the surrounding agent run.
#### Backend integration
Initial public provider:
- `HyperlightCodeActProvider`
Backend-specific notes:
- **Hyperlight**
- The provider internally creates a `SandboxBuilder` from the options and uses the `Sandbox` API from `HyperlightSandbox.Api`.
- The provider uses snapshot/restore to ensure clean execution state per `execute_code` invocation: a "warm" snapshot is taken after the first no-op initialization run, and restored before each subsequent execution.
- File access maps to Hyperlight Sandbox's `WithInputDir()` / `WithOutputDir()` / `WithTempOutput()` capability model.
- Network access is denied by default and is enabled through `Sandbox.AllowDomain(...)` per-target allow-list entries.
- Guest module resolution: if `ModulePath` is null for the Wasm backend, the provider attempts to locate a packaged Python guest module (equivalent to the Python SDK's `python_guest.path` resolution).
#### Capability handling
Capabilities are first-class `HyperlightCodeActProviderOptions` properties and provider-managed CRUD surfaces:
- `WorkspaceRoot`
- `FileMounts`
- `AllowedDomains`
Enabling access means:
- Configuring `WorkspaceRoot` or any `FileMounts` enables the sandbox filesystem surface exposed through `/input` and `/output`.
- Leaving both `WorkspaceRoot` and `FileMounts` unset means no filesystem surface is configured.
- Adding any `AllowedDomains` entry enables outbound access only for the configured targets; leaving it empty means network access is disabled without a separate network mode flag.
Backends may implement stricter semantics than these top-level settings.
#### Execution output representation
Backend execution output maps to a JSON result string returned from the `execute_code` `AIFunction`:
```json
{
"stdout": "Hello world\n",
"stderr": "",
"exit_code": 0,
"success": true
}
```
Execution failures should surface readable error text in the `stderr` field and a non-zero `exit_code`. Timeouts, out-of-memory conditions, backend crashes, and similar sandbox failures are all `execute_code` failures and should surface as structured error results. Partial textual or file outputs may be returned only when the backend can report them unambiguously.
#### `execute_code` input contract
```json
{
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "Code to execute using the provider's configured backend/runtime behavior."
}
},
"required": ["code"]
}
```
#### Thread safety and concurrency
- All CRUD methods (`AddTools`, `RemoveTools`, `AddFileMounts`, etc.) are synchronized via an internal lock.
- `ProvideAIContextAsync(...)` acquires the lock to snapshot current state, then releases it before building the run-scoped function. The run-scoped function closes over the immutable snapshot, not mutable provider state.
- Concurrent `execute_code` invocations from different runs use independent sandbox instances or synchronized access to a shared sandbox with snapshot/restore.
- Workspace directories (`WorkspaceRoot`, `FileMounts`) are external shared state: concurrent runs against the same workspace can race on files. This is the user's responsibility to manage (e.g., by using per-run output directories or separate provider instances).
### HyperlightExecuteCodeFunction
The provider package also exports a standalone `HyperlightExecuteCodeFunction` for direct-tool scenarios where a provider lifecycle is not needed. This is the .NET equivalent of the Python `HyperlightExecuteCodeTool`.
```csharp
/// <summary>
/// A standalone execute_code AIFunction backed by a Hyperlight sandbox.
/// Use this for manual/static wiring when the AIContextProvider lifecycle
/// is not needed.
/// </summary>
public sealed class HyperlightExecuteCodeFunction : IDisposable
{
/// <summary>
/// Creates a new standalone code execution function.
/// </summary>
/// <param name="options">Configuration options.</param>
public HyperlightExecuteCodeFunction(HyperlightCodeActProviderOptions options);
/// <summary>
/// Returns this as an AIFunction for direct registration on an agent.
/// When approval is required, the returned function is wrapped in
/// ApprovalRequiredAIFunction.
/// </summary>
public AIFunction AsAIFunction();
/// <summary>
/// Builds a CodeAct instruction string describing the available
/// tools and capabilities.
/// </summary>
/// <param name="toolsVisibleToModel">
/// When false, the instructions include full tool descriptions
/// (for use when tools are only accessible through CodeAct).
/// When true, instructions are abbreviated (tools are already
/// visible to the model as direct tools).
/// </param>
public string BuildInstructions(bool toolsVisibleToModel = false);
/// <summary>Releases sandbox resources.</summary>
public void Dispose();
}
```
### Internal implementation structure
The provider and standalone function share internal helpers:
```
Microsoft.Agents.AI.Hyperlight/
├── HyperlightCodeActProvider.cs // AIContextProvider implementation
├── HyperlightCodeActProviderOptions.cs // Options record
├── HyperlightExecuteCodeFunction.cs // Standalone AIFunction for manual wiring
├── FileMount.cs // File mount record
├── AllowedDomain.cs // Network allow-list record
├── CodeActApprovalMode.cs // Approval enum
├── Internal/
│ ├── SandboxExecutor.cs // Manages sandbox lifecycle, snapshot/restore
│ ├── InstructionBuilder.cs // Builds CodeAct instruction strings
│ └── ToolBridge.cs // AIFunction ↔ Sandbox.RegisterTool adapter
```
`SandboxExecutor` encapsulates:
- Creating and configuring a `Sandbox` from options.
- Performing the initial no-op warm-up and snapshot.
- Registering bridged tools via `ToolBridge`.
- Restoring to the clean snapshot before each execution.
- Translating `ExecutionResult` to a JSON string.
`InstructionBuilder` generates:
- A short CodeAct guidance block for `AIContext.Instructions`.
- A detailed `execute_code` description including `call_tool(...)` signatures and capability documentation.
`ToolBridge` handles:
- Reflecting `AIFunction` metadata to build the sandbox tool registration.
- The sync-over-async invocation bridge.
## E2E Code Samples
### Tool-enabled CodeAct mode
```csharp
var fetchDocs = AIFunctionFactory.Create(FetchDocs, name: "fetch_docs");
var queryData = AIFunctionFactory.Create(QueryData, name: "query_data");
var lookupUser = AIFunctionFactory.Create(LookupUser, name: "lookup_user");
var codeact = new HyperlightCodeActProvider(new HyperlightCodeActProviderOptions
{
Tools = [fetchDocs, queryData],
WorkspaceRoot = "./workdir",
AllowedDomains = [new AllowedDomain("api.github.com", ["GET"])],
});
codeact.AddTools(lookupUser);
var sendEmail = AIFunctionFactory.Create(SendEmail, name: "send_email");
var agent = chatClient.AsAIAgent(
instructions: "You are a helpful assistant.",
options: new ChatClientAgentOptions
{
Tools = [sendEmail], // direct-only tool
AIContextProviders = [codeact],
});
await using var session = await agent.CreateSessionAsync();
var response = await agent.InvokeAsync("Analyze the latest docs", session);
```
### Standard code interpreter mode
```csharp
var codeact = new HyperlightCodeActProvider(new HyperlightCodeActProviderOptions
{
WorkspaceRoot = "./data",
});
var agent = chatClient.AsAIAgent(
instructions: "You are a code interpreter.",
options: new ChatClientAgentOptions
{
AIContextProviders = [codeact],
});
```
### Manual static wiring (no provider lifecycle)
When the tool registry and capability configuration are fixed, the provider lifecycle can be skipped entirely. Build the `execute_code` function and instructions once and pass them directly to the agent:
```csharp
using var executeCode = new HyperlightExecuteCodeFunction(
new HyperlightCodeActProviderOptions
{
Tools = [fetchDocs, queryData],
WorkspaceRoot = "./workdir",
AllowedDomains = [new AllowedDomain("api.github.com", ["GET"])],
});
var codeactInstructions = executeCode.BuildInstructions(toolsVisibleToModel: false);
var agent = chatClient.AsAIAgent(
instructions: $"You are a helpful assistant.\n\n{codeactInstructions}",
options: new ChatClientAgentOptions
{
Tools = [sendEmail, executeCode.AsAIFunction()],
});
```
### With approval required
```csharp
var sensitiveAction = new ApprovalRequiredAIFunction(
AIFunctionFactory.Create(DeleteRecords, name: "delete_records"));
var codeact = new HyperlightCodeActProvider(new HyperlightCodeActProviderOptions
{
Tools = [fetchDocs, sensitiveAction], // sensitiveAction triggers approval
});
// execute_code will be wrapped in ApprovalRequiredAIFunction because
// at least one managed tool (delete_records) requires approval.
var agent = chatClient.AsAIAgent(
instructions: "You are a helpful assistant.",
options: new ChatClientAgentOptions
{
AIContextProviders = [codeact],
});
```
## Relationship to hyperlight-sandbox .NET SDK
This design depends on the .NET SDK being added in [hyperlight-dev/hyperlight-sandbox#46](https://github.com/hyperlight-dev/hyperlight-sandbox/pull/46). Key types consumed from that SDK:
| hyperlight-sandbox type | Used for |
|---|---|
| `Sandbox` | Core sandbox lifecycle: `Run()`, `RegisterTool()`, `AllowDomain()`, `Snapshot()`, `Restore()` |
| `SandboxBuilder` | Fluent sandbox construction from provider options |
| `SandboxBackend` | Backend selection (Wasm, JavaScript) |
| `ExecutionResult` | Capturing stdout, stderr, exit code from guest execution |
| `SandboxSnapshot` | Checkpoint/restore for clean state per execution |
The provider package (`Microsoft.Agents.AI.Hyperlight`) takes a NuGet dependency on `Hyperlight.HyperlightSandbox.Api` and `Microsoft.Extensions.AI.Abstractions`. It does **not** depend on `HyperlightSandbox.Extensions.AI` (`CodeExecutionTool`) — the provider implements its own sandbox lifecycle management with run-scoped snapshots to support concurrent invocations safely.
## Package structure
The CodeAct Hyperlight provider ships as an optional NuGet package:
- **Package**: `Microsoft.Agents.AI.Hyperlight`
- **Dependencies**:
- `Microsoft.Agents.AI.Abstractions` (for `AIContextProvider`, `AIContext`)
- `Microsoft.Extensions.AI.Abstractions` (for `AIFunction`, `ApprovalRequiredAIFunction`)
- `Hyperlight.HyperlightSandbox.Api` (for sandbox API)
- **Target framework**: `net8.0`
This keeps CodeAct and its native sandbox dependencies optional — users who do not need CodeAct do not take on the Hyperlight installation and dependency footprint.
## Open questions
1. **Guest module distribution**: How should the default Python guest module (`.aot` file) be distributed for .NET consumers? Options include a separate NuGet package with native assets, a runtime download, or requiring users to build/provide their own.
2. **Async tool registration**: If the Hyperlight .NET SDK adds async tool callback support in a future release, the sync-over-async bridge should be replaced. This is tracked as a known technical debt item.
3. **Output file access**: The Hyperlight sandbox exposes `GetOutputFiles()` and `OutputPath` for retrieving files written by guest code. The initial design returns these as part of the JSON result. A future iteration could surface output files as framework-native content (e.g., `DataContent` or URI references).
4. **Multiple sandbox instances for concurrency**: The current design uses synchronized access to a single sandbox with snapshot/restore. An alternative pooling strategy (one sandbox per concurrent run) could improve throughput at the cost of memory. This is deferred to implementation time.
@@ -0,0 +1,385 @@
# CodeAct Python implementation
This document describes the Python realization of the CodeAct design in
[`docs/decisions/0024-codeact-integration.md`](../../decisions/0024-codeact-integration.md).
This document is intentionally focused on the Python design and public API surface.
The initial public Python type described here is `HyperlightCodeActProvider`. Future Python backends, such as Monty, should follow the same conceptual model with their own concrete provider types rather than through a public abstract base class or a public executor parameter.
## What is the goal of this feature?
Goals:
- Python developers can enable CodeAct through a `ContextProvider`-based integration.
- Developers can configure a provider-owned CodeAct tool set that is separate from the agent's direct `tools=` surface.
- Developers can use the same `execute_code` concept for both tool-enabled CodeAct and a standard code interpreter tool implementation.
- Developers can swap execution backends over time, starting with Hyperlight while keeping room for alternatives such as Pydantic's Monty.
- Developers can configure execution capabilities such as workspace mounts and outbound network allow lists in a portable way.
Success Metric:
- Python samples exist for both a tool-enabled CodeAct mode and a standard interpreter mode.
Implementation-free outcome:
- A Python developer can attach a backend-specific CodeAct provider, choose which tools are available inside CodeAct, and configure execution capabilities without rewriting the function invocation loop.
## What is the problem being solved?
The cross-SDK problem statement and decision rationale live in the [ADR](../../decisions/0024-codeact-integration.md). The items below narrow that statement to Python-specific design concerns:
- Today, the easiest way to prototype CodeAct is to infer or reshape the agent's direct tool surface, which is fragile and hard to reason about.
- In Python, inferring a CodeAct tool surface from generic agent tool configuration is fragile and hard to reason about.
- There is no first-class Python design that simultaneously covers Hyperlight-backed CodeAct now, future backend-specific providers such as Monty, and both tool-enabled and interpreter modes.
- Sandbox capabilities such as mounted file access and outbound network access need a portable configuration model instead of ad hoc backend-specific wiring.
- Approval behavior needs to be explicit and configurable, especially when CodeAct and direct tool calling may both be available.
## API Changes
### CodeAct contract
#### Terminology
- **CodeAct** is the primary term.
- **Code mode**, **codemode**, and **programmatic tool calling** refer to the same concept in this document.
- `execute_code` is the model-facing tool name used by the initial Python providers in this spec.
#### Provider-owned CodeAct tool registry
A concrete Python CodeAct provider owns the set of tools available through `call_tool(...)` inside CodeAct.
Rules:
- Only tools explicitly configured on the concrete provider instance are available inside CodeAct.
- The provider must not infer its CodeAct-managed tool set from the agent's direct `tools=` configuration.
- Exclusive versus mixed behavior is achieved by where tools are configured, not by rewriting the agent's direct tool list.
Implications:
- **CodeAct-only tool**: configured on the concrete CodeAct provider only.
- **Direct-only tool**: configured on the agent only.
- **Tool available both ways**: configured on both the agent and the concrete CodeAct provider.
#### Managing tools and capabilities after provider construction
There is no separate runtime setup object in the Python design. CodeAct tools, file mounts, and outbound network allow-list state are managed directly on the provider through CRUD-style registry methods.
Preferred pattern:
- `add_tools(...) -> None`
- `get_tools() -> Sequence[ToolTypes]`
- `remove_tool(...) -> None`
- `clear_tools() -> None`
- `add_file_mounts(...) -> None`
- `get_file_mounts() -> Sequence[FileMount]`
- `remove_file_mount(...) -> None`
- `clear_file_mounts() -> None`
- `add_allowed_domains(...) -> None`
- `get_allowed_domains() -> Sequence[AllowedDomain]`
- `remove_allowed_domain(...) -> None`
- `clear_allowed_domains() -> None`
Requirements:
- The provider-owned CodeAct tool registry is keyed by tool name.
- `add_tools(...)` adds new tools and replaces an existing provider-owned registration when the same tool name is added again.
- `get_tools()` returns the provider's current configured CodeAct tool registry.
- `remove_tool(...)` removes provider-owned CodeAct tools by name.
- `clear_tools()` removes all provider-owned CodeAct tools.
- File mounts are keyed by sandbox mount path.
- `add_file_mounts(...)` adds new file mounts and replaces an existing mount when the same mount path is added again.
- `get_file_mounts()` returns the provider's current configured file mounts.
- `remove_file_mount(...)` removes file mounts by mount path.
- `clear_file_mounts()` removes all configured file mounts.
- Allowed domains are keyed by normalized target string.
- `add_allowed_domains(...)` adds allow-list entries and replaces an existing entry when the same target is added again.
- `get_allowed_domains()` returns the current outbound allow-list entries.
- `remove_allowed_domain(...)` removes allow-list entries by target.
- `clear_allowed_domains()` removes all configured allow-list entries.
- Tool, file-mount, and network-allow-list mutations affect subsequent runs only; runs already in progress keep the snapshot captured at run start.
- The provider must snapshot its effective tool registry and capability state at the start of each run so concurrent execution remains deterministic.
#### Approval model
The initial Python design follows the ADR's initial approval decision and reuses the existing tool approval vocabulary from `agent_framework._tools`:
- `approval_mode="always_require"`
- `approval_mode="never_require"`
The provider exposes a default `approval_mode` for `execute_code`.
Effective `execute_code` approval is computed as follows:
- If the provider default is `always_require`, `execute_code` requires approval.
- If the provider default is `never_require`, the provider evaluates the provider-owned CodeAct tool registry snapshot for that run.
- If every provider-owned CodeAct tool in that snapshot is `never_require`, `execute_code` is `never_require`.
- If any provider-owned CodeAct tool in that snapshot is `always_require`, `execute_code` is `always_require`, even if the generated code may not call that tool.
- Provider-owned tool calls made through `call_tool(...)` during that execution run use the approval already determined for `execute_code`.
- Direct-only agent tools are excluded from this calculation.
- File and network capabilities do not create a separate runtime approval check in the initial model; configuring them on the provider, including adding file mounts or outbound network allow-list entries, is itself the approval for those capabilities.
This is intentionally conservative and matches the shape of the current function-tool approval flow, where `FunctionTool` uses `always_require` / `never_require` and the auto-invocation loop escalates the whole batch if any called tool requires approval.
If one sensitive provider-owned tool causes `execute_code` to require approval more often than desired, the mitigation is to keep that tool direct-only or expose it through a different CodeAct provider/tool surface. The initial model does not try to infer whether generated code will actually call that tool before approval.
If the framework later standardizes pre-execution inspection or nested per-tool approvals, the Python provider surface can grow to expose that explicitly. The initial design does not assume that those extra modes are required.
#### Shared execution flow
On each run:
1. Resolve the provider's backend/runtime behavior, capabilities, provider default `approval_mode`, and provider-owned tool registry.
2. Compute the effective approval requirement for `execute_code` from the provider default plus the provider-owned tool registry snapshot.
3. Build provider-defined instructions.
4. Add `execute_code` to the model-facing tool surface.
5. Invoke the underlying model.
6. When `execute_code` is called, create or reuse an execution environment keyed by provider type, backend setup identity, capability configuration, and provider-owned tool signature.
7. If the current provider mode exposes host tools, expose `call_tool(...)` bound only to the provider-owned tool registry.
8. Execute code and convert results to framework-native content objects.
Caching rules:
- Backends that support snapshots may cache a reusable clean snapshot.
- Backends that do not support snapshots may still cache warm initialization artifacts.
- No mutable per-run execution state may be shared across concurrent runs.
- In-memory interpreter state does not persist across separate `execute_code` calls.
- Configured workspace files, mounted files, and any writable artifact/output area are the supported persistence mechanism across calls when the backend exposes them.
### Python public API
#### Core types
```python
class FileMount(NamedTuple):
host_path: str | Path
mount_path: str
FileMountInput = str | tuple[str | Path, str] | FileMount
class AllowedDomain(NamedTuple):
target: str
methods: tuple[str, ...] | None = None
AllowedDomainInput = str | tuple[str, str | Sequence[str]] | AllowedDomain
class HyperlightCodeActProvider(ContextProvider):
def __init__(
self,
source_id: str = "hyperlight_codeact",
*,
backend: str = "wasm",
module: str | None = "python_guest.path",
module_path: str | None = None,
tools: ToolTypes | None = None,
approval_mode: Literal["always_require", "never_require"] = "never_require",
workspace_root: Path | None = None,
file_mounts: Sequence[FileMountInput] = (),
allowed_domains: Sequence[AllowedDomainInput] = (),
) -> None: ...
def add_tools(self, tools: ToolTypes | Sequence[ToolTypes]) -> None: ...
def get_tools(self) -> Sequence[ToolTypes]: ...
def remove_tool(self, name: str) -> None: ...
def clear_tools(self) -> None: ...
def add_file_mounts(self, mounts: FileMountInput | Sequence[FileMountInput]) -> None: ...
def get_file_mounts(self) -> Sequence[FileMount]: ...
def remove_file_mount(self, mount_path: str) -> None: ...
def clear_file_mounts(self) -> None: ...
def add_allowed_domains(self, domains: AllowedDomainInput | Sequence[AllowedDomainInput]) -> None: ...
def get_allowed_domains(self) -> Sequence[AllowedDomain]: ...
def remove_allowed_domain(self, domain: str) -> None: ...
def clear_allowed_domains(self) -> None: ...
```
`file_mounts` accepts three equivalent input forms:
- `"data/report.csv"` uses the same relative path on the host and in the sandbox.
- `("fixtures/users.json", "data/users.json")` or `(Path("fixtures/users.json"), "data/users.json")` uses distinct host and sandbox paths.
- `FileMount(Path("fixtures/users.json"), "data/users.json")` is the named-tuple form of the explicit pair.
`allowed_domains` accepts three equivalent input forms:
- `"github.com"` allows that target with all backend-supported methods.
- `("github.com", "GET")` or `("github.com", ["GET", "HEAD"])` uses an explicit per-target method list.
- `AllowedDomain("github.com", ("GET", "HEAD"))` is the named-tuple form of the explicit entry.
No public abstract `CodeActContextProvider` base or public `executor=` parameter is required for the initial Python API.
The initial alpha package also exports a standalone `HyperlightExecuteCodeTool`
for direct-tool scenarios where a provider is not needed. That standalone tool
should advertise `call_tool(...)`, the registered sandbox tools, and capability
state through its own `description` rather than requiring separate agent
instructions.
Provider modes:
- If no CodeAct-managed tools are configured, `HyperlightCodeActProvider` uses interpreter-style behavior.
- If one or more CodeAct-managed tools are configured, `HyperlightCodeActProvider` uses tool-enabled behavior.
#### Python provider implementation contract
The concrete provider plugs into the existing Python `ContextProvider` surface from `agent_framework._sessions`.
The Hyperlight package also depends on a small set of core hooks that must remain available from `agent-framework-core`:
- `ContextProvider.before_run(...)`
- `SessionContext.extend_instructions(...)`
- `SessionContext.extend_tools(...)`
- per-run runtime tool access via `SessionContext.options["tools"]`
- the shared `ApprovalMode` vocabulary used by `FunctionTool`
Required lifecycle hook:
- `before_run(*, agent, session, context, state) -> None`
Optional lifecycle hook:
- `after_run(*, agent, session, context, state) -> None`
`before_run(...)` is responsible for:
- snapshotting the current CodeAct-managed tool registry and capability settings for the run,
- computing the effective approval requirement for `execute_code` from the provider default and the snapshotted tool registry,
- adding a short CodeAct guidance block,
- adding `execute_code` to the run through `SessionContext.extend_tools(...)`,
- and wiring any backend-specific execution state needed for the run.
These steps run on every invocation rather than once at construction time because the provider supports CRUD mutations between runs, concurrent runs need independent snapshots, and the effective approval and instructions depend on the tool registry state captured at run start. When the tool registry and capability configuration are fixed for the lifetime of the agent, the manual wiring pattern (see `codeact_manual_wiring.py`) can be used instead, which passes the tool and instructions directly to the `Agent` constructor and avoids the per-run provider lifecycle entirely.
If the provider stores anything in `state`, that value must stay JSON-serializable.
Mutating the provider after `before_run(...)` has captured a run-scoped snapshot is allowed, but it affects subsequent runs only. Provider implementations should synchronize state capture and CRUD operations so shared provider instances remain safe across concurrent runs.
`after_run(...)` is responsible for any backend-specific cleanup or post-processing that must happen after the model invocation completes.
If shared internal helpers are introduced later for multiple concrete providers, they should standardize responsibilities for:
- building instructions,
- computing effective approval,
- configuring file access,
- configuring network access,
- preparing or restoring execution state,
- executing code,
- and converting backend output into framework-native `Content`.
#### Runtime behavior
- `before_run(...)` adds a short CodeAct guidance block through `SessionContext.extend_instructions(...)`.
- `before_run(...)` adds `execute_code` through `SessionContext.extend_tools(...)`.
- The detailed `call_tool(...)`, sandbox-tool, and capability guidance is carried by `execute_code.description`.
- `execute_code` invokes the configured Hyperlight sandbox guest.
- If the current CodeAct tool registry is non-empty, the runtime injects `call_tool(...)` bound to the provider-owned tool registry.
- The provider does not inspect or mutate `Agent.default_options["tools"]` or `context.options["tools"]` to determine its CodeAct tool set.
- The provider snapshots the current CodeAct tool registry and capability state at run start, so later registry and allow-list mutations only affect future runs.
- Interpreter versus tool-enabled behavior is derived from the concrete provider and the presence of CodeAct-managed tools, not from a separate public profile object.
- `execute_code` should be traced like a normal tool invocation within the surrounding agent run, and provider-owned tool calls executed through `call_tool(...)` should continue to emit ordinary tool invocation telemetry.
#### Backend integration
Initial public provider:
- `HyperlightCodeActProvider`
Backend-specific notes:
- **Hyperlight**
- Provider construction needs a guest artifact via `module`, which may be a packaged guest module name or a path to a compiled guest artifact.
- File access maps naturally to Hyperlight Sandbox's read-only `/input` and writable `/output` capability model.
- Network access is denied by default and is enabled through per-target allow-list entries.
- **Monty**
- A future `MontyCodeActProvider` should be a separate public type rather than a `HyperlightCodeActProvider` mode.
- Monty does not expose built-in filesystem or network access directly inside the interpreter.
- File and URL access are mediated through host-provided external functions, so a Monty provider would need to translate provider settings into virtual files and allow-checked callbacks.
- Monty setup may also include backend-specific inputs such as `script_name`, optional type-check stubs, or restored snapshots.
#### Capability handling
Capabilities are first-class `HyperlightCodeActProvider` init parameters and provider-managed CRUD surfaces:
- `workspace_root`
- `file_mounts`
- `allowed_domains`
Concrete providers should normalize these settings internally. Hyperlight can map them directly to sandbox capabilities, while Monty must enforce them through host-mediated file and network functions and may apply stricter URL-level checks than the public provider surface expresses.
Expected management split:
- `workspace_root` remains a direct configuration value on the provider,
- file mounts are managed through provider CRUD methods,
- outbound allow-list entries are managed through provider CRUD methods.
Enabling access means:
- Configuring `workspace_root` or any `file_mounts` enables the sandbox filesystem surface exposed through `/input` and `/output`.
- Leaving both `workspace_root` and `file_mounts` unset means no filesystem surface is configured.
- Adding any `allowed_domains` entry enables outbound access only for the configured targets; leaving it empty means network access is disabled without a separate `network_mode` flag.
- A string target allows all backend-supported methods for that target; an explicit tuple or `AllowedDomain` entry narrows the methods for that target.
Backends may implement stricter semantics than these top-level settings. For example, Hyperlight naturally maps file access to `/input` and `/output`, while Monty would enforce equivalent policy through host-provided callbacks rather than direct interpreter I/O.
#### Execution output representation
Backend execution output should be translated into existing AF `Content` values rather than a custom `CodeActExecutionResult` type.
Use the existing content model from `agent_framework._types`, for example:
- `Content.from_code_interpreter_tool_result(outputs=[...])` to surface the overall result of sandboxed code execution,
- `Content.from_text(...)` for plain textual output,
- `Content.from_data(...)` or `Content.from_uri(...)` for generated files or binary artifacts,
- `Content.from_error(...)` for execution failures,
- and `Content.from_function_result(..., result=list[Content])` when surfacing the final result of `execute_code` through the normal tool result path.
#### `execute_code` input contract
```json
{
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "Code to execute using the provider's configured backend/runtime behavior."
}
},
"required": ["code"]
}
```
Execution failures should surface readable error text and structured error `Content`, not a custom backend result object.
Timeouts, out-of-memory conditions, backend crashes, and similar sandbox failures are all `execute_code` failures and should surface as structured error content. Partial textual or file outputs may be returned only when the backend can report them unambiguously; callers should not rely on partial-output recovery as a portable contract.
## E2E Code Samples
### Tool-enabled CodeAct mode
```python
codeact = HyperlightCodeActProvider(
tools=[fetch_docs, query_data],
workspace_root="./workdir",
allowed_domains=[("api.github.com", "GET")],
)
codeact.add_tools([lookup_user])
agent = Agent(
client=client,
name="assistant",
tools=[send_email], # direct-only tool
context_providers=[codeact],
)
```
### Standard code interpreter mode
```python
codeact = HyperlightCodeActProvider(
workspace_root="./data",
)
agent = Agent(
client=client,
name="interpreter",
context_providers=[codeact],
)
```
### Manual static wiring (no per-run provider lifecycle)
When the tool registry and capability configuration are fixed, the provider lifecycle can be skipped entirely. Build the `execute_code` tool and instructions once and pass them directly to the agent:
```python
execute_code = HyperlightExecuteCodeTool(
tools=[fetch_docs, query_data],
workspace_root="./workdir",
allowed_domains=[("api.github.com", "GET")],
approval_mode="never_require",
)
codeact_instructions = execute_code.build_instructions(tools_visible_to_model=False)
agent = Agent(
client=client,
name="assistant",
instructions=f"You are a helpful assistant.\n\n{codeact_instructions}",
tools=[send_email, execute_code],
)
```
@@ -177,7 +177,7 @@ This feature ports the vector store abstractions, embedding generator abstractio
**Goal:** Add embedding generators to all existing AF provider packages that have chat clients.
**Mergeable:** Yes — each is independent, added to existing provider packages.
#### 2.1 — Azure AI Inference embedding (in `packages/azure-ai/`)
#### 2.1 — Foundry inference embedding (in `packages/foundry/`)
#### 2.2 — Ollama embedding (in `packages/ollama/`)
#### 2.3 — Anthropic embedding (in `packages/anthropic/`)
#### 2.4 — Bedrock embedding (in `packages/bedrock/`)
+2 -2
View File
@@ -12,8 +12,8 @@ dotnet/
│ ├── Microsoft.Agents.AI.Abstractions/ # Core AI agent abstractions
│ ├── Microsoft.Agents.AI.A2A/ # Agent-to-Agent (A2A) provider
│ ├── Microsoft.Agents.AI.OpenAI/ # OpenAI provider
│ ├── Microsoft.Agents.AI.AzureAI/ # Azure AI Foundry Agents (v2) provider
│ ├── Microsoft.Agents.AI.AzureAI.Persistent/ # Legacy Azure AI Foundry Agents (v1) provider
│ ├── Microsoft.Agents.AI.Foundry/ # Microsoft Foundry Agents (v2) provider
│ ├── Microsoft.Agents.AI.AzureAI.Persistent/ # Legacy Microsoft Foundry Agents (v1) provider
│ ├── Microsoft.Agents.AI.Anthropic/ # Anthropic provider
│ ├── Microsoft.Agents.AI.Workflows/ # Workflow orchestration
│ └── ... # Other packages
+225
View File
@@ -0,0 +1,225 @@
---
name: verify-samples-tool
description: How to use the verify-samples tool to run, verify, and manage sample definitions in the Agent Framework repository. Use this when adding, updating, or running sample verification.
---
# verify-samples Tool
The `verify-samples` project (`dotnet/eng/verify-samples/`) is an automated tool that runs sample projects and verifies their output using deterministic checks and AI-powered verification.
## Running verify-samples
**Important:** By default, samples must be pre-built before running verify-samples. Build the solution first, or pass `--build` to build samples during the run:
```bash
cd dotnet
dotnet build agent-framework-dotnet.slnx -f net10.0
```
Then run verify-samples:
```bash
# Run all samples across all categories
dotnet run --project eng/verify-samples -- --log results.log --csv results.csv
# Run a specific category
dotnet run --project eng/verify-samples -- --category 02-agents --log results.log
# Run specific samples by name
dotnet run --project eng/verify-samples -- Agent_Step02_StructuredOutput Agent_Step09_AsFunctionTool
# Control parallelism (default 8)
dotnet run --project eng/verify-samples -- --parallel 8 --log results.log
# Build samples during run (skips the need for a prior build step)
# This may cause build conflicts as multiple samples are built in parallel, so use with caution
dotnet run --project eng/verify-samples -- --build --log results.log
# Combine options
dotnet run --project eng/verify-samples -- --category 03-workflows --parallel 4 --log results.log --csv results.csv --md results.md
```
### Required Environment Variables
The tool itself needs:
- `AZURE_OPENAI_ENDPOINT` — for the AI verification agent
- `AZURE_OPENAI_DEPLOYMENT_NAME` (optional, defaults to `gpt-5-mini`)
Individual samples require their own env vars (e.g., `AZURE_AI_PROJECT_ENDPOINT`). The tool automatically checks and skips samples with missing env vars.
### Output Files
- `--log results.log` — detailed per-sample log with stdout/stderr, AI reasoning, and a summary
- `--csv results.csv` — tabular summary with Sample, ProjectPath, Status, FailedChecks, and Failures columns
- `--md results.md` — Markdown summary with results table and collapsible failure details (suitable for GitHub PR comments)
## Sample Categories
Definitions are in the `dotnet/eng/verify-samples/` directory:
| Category | Config File | Registered Key |
|----------|-------------|----------------|
| 01-get-started | `GetStartedSamples.cs` | `01-get-started` |
| 02-agents | `AgentsSamples.cs` | `02-agents` |
| 03-workflows | `WorkflowSamples.cs` | `03-workflows` |
Categories are registered in `VerifyOptions.cs` in the `s_sampleSets` dictionary.
## SampleDefinition Properties
Each sample is defined as a `SampleDefinition` in the appropriate config file. Key properties:
```csharp
new SampleDefinition
{
// Required: Display name for the sample
Name = "Agent_Step02_StructuredOutput",
// Required: Relative path from dotnet/ to the sample project directory
ProjectPath = "samples/02-agents/Agents/Agent_Step02_StructuredOutput",
// Environment variables the sample requires (throws if missing)
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
// Environment variables with defaults that would prompt on console if unset
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
// Skip this sample with a reason (for structural issues only)
SkipReason = null, // or "Requires external service X."
// Deterministic checks: substrings that must appear in stdout
MustContain = ["=== Section Header ==="],
// Substrings that must NOT appear in stdout
MustNotContain = [],
// If true, only MustContain checks are used (no AI verification)
IsDeterministic = false,
// AI verification: natural-language descriptions of expected output
// Each entry describes one aspect to verify independently
ExpectedOutputDescription =
[
"The output should show structured person information with Name, Age, and Occupation fields.",
"The output should not contain error messages or stack traces.",
],
// Stdin inputs to feed to the sample (for interactive samples)
Inputs = ["Y", "Y", "Y"],
// Delay between stdin inputs in ms (default 2000, increase for LLM calls between inputs)
InputDelayMs = 3000,
}
```
## How to Add a New Sample Definition
1. **Check the sample's Program.cs** to understand:
- What environment variables it reads (look for `GetEnvironmentVariable`)
- Whether it needs stdin input (look for `Console.ReadLine`, `Application.GetInput`)
- Whether it has an external loop (look for `EXIT` patterns in YAML workflows)
- What output it produces (section headers, markers, expected behavior)
- Whether it exits on its own or runs as a server
2. **Choose the right verification strategy:**
- **Deterministic** (`IsDeterministic = true`): Use `MustContain` for samples with fixed output strings. No AI verification.
- **AI-verified** (default): Use `ExpectedOutputDescription` with semantic descriptions. Write expectations that are flexible enough for non-deterministic LLM output.
- **Both**: Use `MustContain` for fixed markers AND `ExpectedOutputDescription` for LLM-generated content.
3. **Set `SkipReason` only for structural issues:**
- Web servers that don't exit
- Multi-process client/server architectures
- Samples requiring external infrastructure (MCP servers you can't reach, Docker, etc.)
- Do NOT skip for missing env vars — the tool checks those dynamically.
4. **For interactive samples, provide `Inputs`:**
- Samples using `Application.GetInput(args)` need one initial input
- Samples with `Console.ReadLine()` approval loops need `"Y"` inputs
- YAML workflows with `externalLoop` need `"EXIT"` as the last input
- Set `InputDelayMs` to 3000-8000ms for samples with LLM calls between inputs
5. **Add the definition** to the appropriate config file (e.g., `AgentsSamples.cs`) in the `All` list.
6. **Register new categories** (if needed) in `VerifyOptions.cs` `s_sampleSets` dictionary.
### Writing Good ExpectedOutputDescription
- Write descriptions that are **semantically flexible** — LLM output varies between runs
- Each array entry should describe **one independent aspect** to verify
- Always include `"The output should not contain error messages or stack traces."` as the last entry
- Avoid exact wording expectations — use "should mention", "should contain information about", "should show"
- Bad: `"The output should say 'The weather in Amsterdam is cloudy with a high of 15°C'"`
- Good: `"The output should contain weather information about Amsterdam mentioning cloudy weather with a high of 15°C."`
### Example: Simple LLM Sample
```csharp
new SampleDefinition
{
Name = "Agent_With_AzureOpenAIChatCompletion",
ProjectPath = "samples/02-agents/AgentProviders/Agent_With_AzureOpenAIChatCompletion",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
ExpectedOutputDescription =
[
"The output should contain a joke about a pirate.",
"The output should not contain error messages or stack traces.",
],
},
```
### Example: Deterministic Sample
```csharp
new SampleDefinition
{
Name = "Workflow_Declarative_GenerateCode",
ProjectPath = "samples/03-workflows/Declarative/GenerateCode",
IsDeterministic = true,
MustContain = ["WORKFLOW: Parsing", "WORKFLOW: Defined"],
ExpectedOutputDescription = ["The output should show a YAML workflow being parsed and C# code being generated from it."],
},
```
### Example: Interactive Sample with Approval Loop
```csharp
new SampleDefinition
{
Name = "FoundryAgent_Hosted_MCP",
ProjectPath = "samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
Inputs = ["Y", "Y", "Y", "Y", "Y"],
InputDelayMs = 5000,
ExpectedOutputDescription = ["The output should show an agent using the Microsoft Learn MCP tool with approval prompts."],
},
```
### Example: Declarative Workflow with External Loop
```csharp
new SampleDefinition
{
Name = "Workflow_Declarative_FunctionTools",
ProjectPath = "samples/03-workflows/Declarative/FunctionTools",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
Inputs = ["What are today's specials?", "EXIT"],
InputDelayMs = 8000,
ExpectedOutputDescription = ["The output should show a workflow calling function tools to answer a question about restaurant specials."],
},
```
### Example: Skipped Sample
```csharp
new SampleDefinition
{
Name = "Agent_MCP_Server",
ProjectPath = "samples/02-agents/ModelContextProtocol/Agent_MCP_Server",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
SkipReason = "Runs as an MCP stdio server that does not exit on its own.",
},
```
+8 -1
View File
@@ -402,4 +402,11 @@ FodyWeavers.xsd
*.msp
# JetBrains Rider
*.sln.iml
*.sln.iml
# Foundry agent CLI config (contains secrets, auto-generated)
.foundry-agent.json
.foundry-agent-build.log
# Pre-published output for Docker builds
out/
+3 -2
View File
@@ -29,13 +29,14 @@ using types like `IChatClient`, `FunctionInvokingChatClient`, `AITool`, `AIFunct
## Key Conventions
- **Encoding**: All new files must be saved with UTF-8 encoding with BOM (Byte Order Mark). This is required for `dotnet format` to work correctly.
- **Command output capture**: When running `dotnet build`, `dotnet test`, `dotnet format`, or similar commands, redirect output to a temp file first (e.g., `dotnet build --tl:off 2>&1 | Out-File $env:TEMP\build.log`), then analyze the file as needed. This avoids re-running expensive commands when the initial analysis misses something.
- **Encoding**: All new files must be saved with UTF-8 encoding with BOM (Byte Order Mark). This is required for `dotnet format` to work correctly. When using PowerShell `Set-Content`, always pass `-Encoding UTF8BOM` to preserve the BOM (e.g., `Set-Content $file $content -NoNewline -Encoding UTF8BOM`).
- **Copyright header**: `// Copyright (c) Microsoft. All rights reserved.` at top of all `.cs` files
- **XML docs**: Required for all public methods and classes
- **Async**: Use `Async` suffix for methods returning `Task`/`ValueTask`
- **Private classes**: Should be `sealed` unless subclassed
- **Config**: Read from environment variables with `UPPER_SNAKE_CASE` naming
- **Tests**: Add Arrange/Act/Assert comments; use Moq for mocking
- **Tests**: Add Arrange/Act/Assert comments; use Moq for mocking; test methods returning `Task`/`ValueTask` must use the `Async` suffix.
## Key Design Principles
+1
View File
@@ -17,6 +17,7 @@
<PropertyGroup>
<IsReleaseCandidate>false</IsReleaseCandidate>
<IsReleased>false</IsReleased>
</PropertyGroup>
<PropertyGroup>
+3 -2
View File
@@ -4,8 +4,9 @@
<!-- https://learn.microsoft.com/en-us/nuget/consume-packages/Central-Package-Management -->
<Sdk Name="Microsoft.Build.CentralPackageVersions" Version="2.1.3" />
<!-- Only run 'dotnet format' on dev machines, Release builds. Skip on GitHub Actions -->
<!-- as this runs in its own Actions job. -->
<Target Name="DotnetFormatOnBuild" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Release' AND '$(GITHUB_ACTIONS)' == '' ">
<!-- as this runs in its own Actions job. Only run for net10.0 target frameworks since the dotnet format command -->
<!-- already formats all target frameworks in project. Otherwise it will run format x times x where x is the number of target frameworks -->
<Target Name="DotnetFormatOnBuild" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Release' AND '$(GITHUB_ACTIONS)' == '' AND '$(TargetFramework)' == 'net10.0' ">
<Message Text="Running dotnet format" Importance="high" />
<Exec Command="dotnet format --no-restore -v diag $(ProjectFileName)" />
</Target>
+55 -55
View File
@@ -7,93 +7,95 @@
</PropertyGroup>
<PropertyGroup>
<!-- Aspire -->
<AspireAppHostSdkVersion>13.0.2</AspireAppHostSdkVersion>
<AspireAppHostSdkVersion>13.1.0</AspireAppHostSdkVersion>
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="12.8.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.4.2" />
<PackageVersion Include="Anthropic" Version="12.13.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.5.0" />
<PackageVersion Include="Aspire.Hosting" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
<PackageVersion Include="Aspire.Azure.AI.Inference" Version="13.1.0-preview.1.25616.3" />
<PackageVersion Include="Aspire.Hosting.Azure.AIFoundry" Version="13.1.0-preview.1.25616.3" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.Projects" Version="2.0.0-beta.1" />
<PackageVersion Include="Azure.AI.Projects.OpenAI" Version="2.0.0-beta.1" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.8" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.8.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.22" />
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.1" />
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.3" />
<PackageVersion Include="Azure.AI.Projects" Version="2.0.0" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
<PackageVersion Include="Azure.Core" Version="1.53.0" />
<PackageVersion Include="Azure.Identity" Version="1.21.0" />
<PackageVersion Include="DotNetEnv" Version="3.1.1" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.5.0" />
<!-- Google Gemini -->
<PackageVersion Include="Google.GenAI" Version="0.11.0" />
<PackageVersion Include="Google.GenAI" Version="1.6.0" />
<PackageVersion Include="Mscc.GenerativeAI.Microsoft" Version="2.9.3" />
<!-- Microsoft.Azure.* -->
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.54.0" />
<!-- Newtonsoft.Json -->
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<!-- System.* -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.4" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.6" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.4" />
<PackageVersion Include="System.ClientModel" Version="1.9.0" />
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.5" />
<PackageVersion Include="System.ClientModel" Version="1.10.0" />
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.3" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.4" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.6" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.5" />
<PackageVersion Include="System.Net.Http.Json" Version="10.0.0" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.3" />
<PackageVersion Include="System.Text.Json" Version="10.0.3" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.3" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.5" />
<PackageVersion Include="System.Text.Json" Version="10.0.6" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.6" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
<!-- OpenTelemetry -->
<PackageVersion Include="OpenTelemetry" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" />
<PackageVersion Include="OpenTelemetry" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.14.0" />
<!-- Microsoft.AspNetCore.* -->
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
<!-- Microsoft.Extensions.* -->
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.3.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.3.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation" Version="10.3.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Quality" Version="10.3.0" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.5.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.5.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Quality" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Safety" Version="10.3.0-preview.1.26109.11" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.3.0" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.5.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Compliance.Abstractions" Version="10.5.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="9.7.0" />
<!-- Vector Stores -->
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.67.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.67.0-preview" />
<!-- Semantic Kernel -->
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.67.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.67.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.OpenAI" Version="1.67.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.67.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.67.0" />
<!-- Agent SDKs -->
<PackageVersion Include="GitHub.Copilot.SDK" Version="0.1.29" />
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.171-beta" />
@@ -102,18 +104,17 @@
<PackageVersion Include="Microsoft.Agents.Authentication.Msal" Version="1.3.171-beta" />
<PackageVersion Include="Microsoft.Agents.Hosting.AspNetCore" Version="1.3.171-beta" />
<!-- A2A -->
<PackageVersion Include="A2A" Version="0.3.4-preview" />
<PackageVersion Include="A2A.AspNetCore" Version="0.3.4-preview" />
<PackageVersion Include="A2A" Version="1.0.0-preview2" />
<PackageVersion Include="A2A.AspNetCore" Version="1.0.0-preview2" />
<!-- MCP -->
<PackageVersion Include="ModelContextProtocol" Version="1.1.0" />
<!-- Inference SDKs -->
<PackageVersion Include="AWSSDK.Extensions.Bedrock.MEAI" Version="4.0.5.1" />
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.10.0" />
<PackageVersion Include="Microsoft.ML.Tokenizers" Version="2.0.0" />
<PackageVersion Include="OllamaSharp" Version="5.4.8" />
<PackageVersion Include="OpenAI" Version="2.8.0" />
<PackageVersion Include="OpenAI" Version="2.10.0" />
<!-- Identity -->
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.78.0" />
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.83.1" />
<!-- Workflows -->
<PackageVersion Include="Microsoft.Agents.ObjectModel" Version="2026.2.4.1" />
<PackageVersion Include="Microsoft.Agents.ObjectModel.Json" Version="2026.2.4.1" />
@@ -126,7 +127,6 @@
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.18.0" />
<!-- Azure Functions -->
<PackageVersion Include="Microsoft.Azure.Functions.Worker" Version="2.50.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.12.1" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.1" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
+127 -60
View File
@@ -4,9 +4,13 @@
<BuildType Name="Publish" />
<BuildType Name="Release" />
</Configurations>
<Folder Name="/src/Aspire.Hosting.AgentFramework.DevUI/">
<Project Path="src/Aspire.Hosting.AgentFramework.DevUI/Aspire.Hosting.AgentFramework.DevUI.csproj" />
</Folder>
<Folder Name="/Samples/">
<File Path="samples/AGENTS.md" />
<File Path="samples/README.md" />
<Project Path="eng/verify-samples/verify-samples.csproj" />
</Folder>
<Folder Name="/Samples/01-get-started/">
<Project Path="samples/01-get-started/01_hello_agent/01_hello_agent.csproj" />
@@ -33,10 +37,15 @@
<Project Path="samples/02-agents/AgentProviders/Agent_With_GoogleGemini/Agent_With_GoogleGemini.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_Ollama/Agent_With_Ollama.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_ONNX/Agent_With_ONNX.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIAssistants/Agent_With_OpenAIAssistants.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj" />
</Folder>
<Folder Name="/Samples/05-end-to-end/DevUIAspireIntegration/">
<Project Path="samples/05-end-to-end/DevUIAspireIntegration/DevUIIntegration.AppHost/DevUIIntegration.AppHost.csproj" />
<Project Path="samples/05-end-to-end/DevUIAspireIntegration/DevUIIntegration.ServiceDefaults/DevUIIntegration.ServiceDefaults.csproj" />
<Project Path="samples/05-end-to-end/DevUIAspireIntegration/EditorAgent/EditorAgent.csproj" />
<Project Path="samples/05-end-to-end/DevUIAspireIntegration/WriterAgent/WriterAgent.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/Agents/">
<File Path="samples/02-agents/Agents/README.md" />
<Project Path="samples/02-agents/Agents/Agent_Step01_UsingFunctionToolsWithApprovals/Agent_Step01_UsingFunctionToolsWithApprovals.csproj" />
@@ -57,6 +66,7 @@
<Project Path="samples/02-agents/Agents/Agent_Step16_Declarative/Agent_Step16_Declarative.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step17_AdditionalAIContext/Agent_Step17_AdditionalAIContext.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Agent_Step18_CompactionPipeline.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step19_InFunctionLoopCheckpointing/Agent_Step19_InFunctionLoopCheckpointing.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/DeclarativeAgents/">
<Project Path="samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
@@ -76,6 +86,8 @@
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/01_SequentialWorkflow.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/03_WorkflowHITL.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/04_WorkflowMcpTool.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/05_WorkflowAndAgents.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/">
<File Path="samples/GettingStarted/README.md" />
@@ -101,7 +113,11 @@
</Folder>
<Folder Name="/Samples/02-agents/AgentSkills/">
<File Path="samples/02-agents/AgentSkills/README.md" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step01_BasicSkills/Agent_Step01_BasicSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills/Agent_Step01_FileBasedSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step02_CodeDefinedSkills/Agent_Step02_CodeDefinedSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AGUI/Step05_StateManagement/">
<Project Path="samples/02-agents/AGUI/Step05_StateManagement/Client/Client.csproj" />
@@ -118,6 +134,41 @@
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj" />
<Project Path="samples/02-agents/AgentWithAnthropic/Agent_Anthropic_Step04_UsingSkills/Agent_Anthropic_Step04_UsingSkills.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AgentsWithFoundry/">
<File Path="samples/02-agents/AgentsWithFoundry/README.md" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step00_FoundryAgentLifecycle/Agent_Step00_FoundryAgentLifecycle.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step01_Basics/Agent_Step01_Basics.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step02.1_MultiturnConversation/Agent_Step02.1_MultiturnConversation.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step02.2_MultiturnWithServerConversations/Agent_Step02.2_MultiturnWithServerConversations.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step07_Observability/Agent_Step07_Observability.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step08_DependencyInjection/Agent_Step08_DependencyInjection.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step09_UsingMcpClientAsTools/Agent_Step09_UsingMcpClientAsTools.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step10_UsingImages/Agent_Step10_UsingImages.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step11_AsFunctionTool/Agent_Step11_AsFunctionTool.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step12_Middleware/Agent_Step12_Middleware.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step13_Plugins/Agent_Step13_Plugins.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step14_CodeInterpreter/Agent_Step14_CodeInterpreter.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step15_ComputerUse/Agent_Step15_ComputerUse.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step16_FileSearch/Agent_Step16_FileSearch.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step17_OpenAPITools/Agent_Step17_OpenAPITools.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step18_BingCustomSearch/Agent_Step18_BingCustomSearch.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step19_SharePoint/Agent_Step19_SharePoint.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step20_MicrosoftFabric/Agent_Step20_MicrosoftFabric.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step21_WebSearch/Agent_Step21_WebSearch.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step22_MemorySearch/Agent_Step22_MemorySearch.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step23_LocalMCP/Agent_Step23_LocalMCP.csproj" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step24_CodeInterpreterFileDownload/Agent_Step24_CodeInterpreterFileDownload.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/Evaluation/">
<Project Path="samples/02-agents/Evaluation/Evaluation_SimpleEval/Evaluation_SimpleEval.csproj" />
<Project Path="samples/02-agents/Evaluation/Evaluation_CustomEvals/Evaluation_CustomEvals.csproj" />
<Project Path="samples/02-agents/Evaluation/Evaluation_ExpectedOutputs/Evaluation_ExpectedOutputs.csproj" />
<Project Path="samples/02-agents/Evaluation/Evaluation_Multimodal/Evaluation_Multimodal.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AgentWithMemory/">
<File Path="samples/02-agents/AgentWithMemory/README.md" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/AgentWithMemory_Step01_ChatHistoryMemory.csproj" />
@@ -132,6 +183,7 @@
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step03_CreateFromChatClient/Agent_OpenAI_Step03_CreateFromChatClient.csproj" />
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient/Agent_OpenAI_Step04_CreateFromOpenAIResponseClient.csproj" />
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step05_Conversation/Agent_OpenAI_Step05_Conversation.csproj" />
<Project Path="samples/02-agents/AgentWithOpenAI/Agent_OpenAI_Step06_CodeInterpreterFileDownload/Agent_OpenAI_Step06_CodeInterpreterFileDownload.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AgentWithRAG/">
<File Path="samples/02-agents/AgentWithRAG/README.md" />
@@ -139,35 +191,7 @@
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step02_CustomVectorStoreRAG/AgentWithRAG_Step02_CustomVectorStoreRAG.csproj" />
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step03_CustomRAGDataSource/AgentWithRAG_Step03_CustomRAGDataSource.csproj" />
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step04_FoundryServiceRAG/AgentWithRAG_Step04_FoundryServiceRAG.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/FoundryAgents/">
<File Path="samples/02-agents/FoundryAgents/README.md" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Evaluations_Step01_RedTeaming/FoundryAgents_Evaluations_Step01_RedTeaming.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Evaluations_Step02_SelfReflection/FoundryAgents_Evaluations_Step02_SelfReflection.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step01.1_Basics/FoundryAgents_Step01.1_Basics.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step01.2_Running/FoundryAgents_Step01.2_Running.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step02_MultiturnConversation/FoundryAgents_Step02_MultiturnConversation.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/FoundryAgents_Step03_UsingFunctionTools.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step04_UsingFunctionToolsWithApprovals/FoundryAgents_Step04_UsingFunctionToolsWithApprovals.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step05_StructuredOutput/FoundryAgents_Step05_StructuredOutput.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step06_PersistedConversations/FoundryAgents_Step06_PersistedConversations.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step07_Observability/FoundryAgents_Step07_Observability.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step08_DependencyInjection/FoundryAgents_Step08_DependencyInjection.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step09_UsingMcpClientAsTools/FoundryAgents_Step09_UsingMcpClientAsTools.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step10_UsingImages/FoundryAgents_Step10_UsingImages.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step11_AsFunctionTool/FoundryAgents_Step11_AsFunctionTool.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step12_Middleware/FoundryAgents_Step12_Middleware.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step13_Plugins/FoundryAgents_Step13_Plugins.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step14_CodeInterpreter/FoundryAgents_Step14_CodeInterpreter.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step15_ComputerUse/FoundryAgents_Step15_ComputerUse.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step16_FileSearch/FoundryAgents_Step16_FileSearch.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step17_OpenAPITools/FoundryAgents_Step17_OpenAPITools.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step18_BingCustomSearch/FoundryAgents_Step18_BingCustomSearch.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step19_SharePoint/FoundryAgents_Step19_SharePoint.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step20_MicrosoftFabric/FoundryAgents_Step20_MicrosoftFabric.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step21_WebSearch/FoundryAgents_Step21_WebSearch.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step22_MemorySearch/FoundryAgents_Step22_MemorySearch.csproj" />
<Project Path="samples/02-agents/FoundryAgents/FoundryAgents_Step23_LocalMCP/FoundryAgents_Step23_LocalMCP.csproj" />
<Project Path="samples/02-agents/AgentWithRAG/AgentWithRAG_Step05_Neo4jGraphRAG/AgentWithRAG_Step05_Neo4jGraphRAG.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/ModelContextProtocol/">
<File Path="samples/02-agents/ModelContextProtocol/README.md" />
@@ -209,12 +233,12 @@
<Project Path="samples/03-workflows/Declarative/ToolApproval/ToolApproval.csproj" />
</Folder>
<Folder Name="/Samples/03-workflows/Declarative/Examples/">
<File Path="../workflow-samples/CustomerSupport.yaml" />
<File Path="../workflow-samples/DeepResearch.yaml" />
<File Path="../workflow-samples/Marketing.yaml" />
<File Path="../workflow-samples/MathChat.yaml" />
<File Path="../workflow-samples/README.md" />
<File Path="../workflow-samples/wttr.json" />
<File Path="../declarative-agents/workflow-samples/CustomerSupport.yaml" />
<File Path="../declarative-agents/workflow-samples/DeepResearch.yaml" />
<File Path="../declarative-agents/workflow-samples/Marketing.yaml" />
<File Path="../declarative-agents/workflow-samples/MathChat.yaml" />
<File Path="../declarative-agents/workflow-samples/README.md" />
<File Path="../declarative-agents/workflow-samples/wttr.json" />
</Folder>
<Folder Name="/Samples/03-workflows/SharedStates/">
<Project Path="samples/03-workflows/SharedStates/SharedStates.csproj" />
@@ -236,6 +260,9 @@
<Folder Name="/Samples/03-workflows/HumanInTheLoop/">
<Project Path="samples/03-workflows/HumanInTheLoop/HumanInTheLoopBasic/HumanInTheLoopBasic.csproj" />
</Folder>
<Folder Name="/Samples/03-workflows/Orchestration/">
<Project Path="samples/03-workflows/Orchestration/Handoff/Handoff.csproj" />
</Folder>
<Folder Name="/Samples/03-workflows/Observability/">
<Project Path="samples/03-workflows/Observability/ApplicationInsights/ApplicationInsights.csproj" />
<Project Path="samples/03-workflows/Observability/AspireDashboard/AspireDashboard.csproj" />
@@ -253,7 +280,47 @@
<Project Path="samples/03-workflows/_StartHere/06_MixedWorkflowAgentsAndExecutors/06_MixedWorkflowAgentsAndExecutors.csproj" />
<Project Path="samples/03-workflows/_StartHere/07_WriterCriticWorkflow/07_WriterCriticWorkflow.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/" />
<Folder Name="/Samples/03-workflows/Evaluation/">
<Project Path="samples/03-workflows/Evaluation/Evaluation_WorkflowEval/Evaluation_WorkflowEval.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/">
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/" />
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/invocations/" />
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/">
<Project Path="samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/Hosted-Invocations-EchoAgent.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/invocations/Using-Samples/">
<Project Path="samples/04-hosting/FoundryHostedAgents/invocations/Using-Samples/SimpleInvocationsAgent/SimpleInvocationsAgent.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/" />
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/HostedFoundryAgent.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/HostedLocalTools.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/HostedMcpTools.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/HostedToolbox.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/HostedTextRag.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/HostedWorkflowSimple.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/SimpleAgent.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/HostedWorkflowHandoff.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/DurableAgents/" />
<Folder Name="/Samples/04-hosting/DurableAgents/AzureFunctions/">
<File Path="samples/04-hosting/DurableAgents/AzureFunctions/.editorconfig" />
@@ -277,15 +344,22 @@
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/06_LongRunningTools/06_LongRunningTools.csproj" />
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/07_ReliableStreaming/07_ReliableStreaming.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/A2A/">
<File Path="samples/04-hosting/A2A/README.md" />
<Project Path="samples/04-hosting/A2A/A2AAgent_AsFunctionTools/A2AAgent_AsFunctionTools.csproj" />
<Project Path="samples/04-hosting/A2A/A2AAgent_PollingForTaskCompletion/A2AAgent_PollingForTaskCompletion.csproj" />
<Folder Name="/Samples/02-agents/A2A/">
<File Path="samples/02-agents/A2A/README.md" />
<Project Path="samples/02-agents/A2A/A2AAgent_AsFunctionTools/A2AAgent_AsFunctionTools.csproj" />
<Project Path="samples/02-agents/A2A/A2AAgent_PollingForTaskCompletion/A2AAgent_PollingForTaskCompletion.csproj" />
<Project Path="samples/02-agents/A2A/A2AAgent_StreamReconnection/A2AAgent_StreamReconnection.csproj" />
<Project Path="samples/02-agents/A2A/A2AAgent_ProtocolSelection/A2AAgent_ProtocolSelection.csproj" />
</Folder>
<Folder Name="/Samples/05-end-to-end/">
<Project Path="samples/05-end-to-end/AgentWithPurview/AgentWithPurview.csproj" />
<Project Path="samples/05-end-to-end/M365Agent/M365Agent.csproj" />
</Folder>
<Folder Name="/Samples/05-end-to-end/Evaluation/">
<Project Path="samples/05-end-to-end/Evaluation/Evaluation_FoundryQuality/Evaluation_FoundryQuality.csproj" />
<Project Path="samples/05-end-to-end/Evaluation/Evaluation_MixedProviders/Evaluation_MixedProviders.csproj" />
<Project Path="samples/05-end-to-end/Evaluation/Evaluation_ConversationSplits/Evaluation_ConversationSplits.csproj" />
</Folder>
<Folder Name="/Samples/05-end-to-end/A2AClientServer/">
<File Path="samples/05-end-to-end/A2AClientServer/README.md" />
<Project Path="samples/05-end-to-end/A2AClientServer/A2AClient/A2AClient.csproj" />
@@ -303,21 +377,11 @@
<Project Path="samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj" />
<Project Path="samples/05-end-to-end/AGUIClientServer/AGUIServer/AGUIServer.csproj" />
</Folder>
<Folder Name="/Samples/05-end-to-end/HostedAgents/">
<Project Path="samples/05-end-to-end/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/AgentThreadAndHITL/AgentThreadAndHITL.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/AgentWithLocalTools/AgentWithLocalTools.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/AgentWithTools/AgentWithTools.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj" />
<Project Path="samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj" />
</Folder>
<Folder Name="/Samples/05-end-to-end/AspNetAgentAuthorization/">
<File Path="samples/05-end-to-end/AspNetAgentAuthorization/docker-compose.yml" />
<File Path="samples/05-end-to-end/AspNetAgentAuthorization/README.md" />
<Project Path="samples/05-end-to-end/AspNetAgentAuthorization/Service/Service.csproj" />
<Project Path="samples/05-end-to-end/AspNetAgentAuthorization/RazorWebClient/RazorWebClient.csproj" />
<Project Path="samples/05-end-to-end/AspNetAgentAuthorization/Service/Service.csproj" />
</Folder>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
@@ -453,6 +517,10 @@
<File Path="src/Shared/Samples/TextOutputHelperExtensions.cs" />
<File Path="src/Shared/Samples/XunitLogger.cs" />
</Folder>
<Folder Name="/Solution Items/src/Shared/Redaction/">
<File Path="src/Shared/Redaction/README.md" />
<File Path="src/Shared/Redaction/ReplacingRedactor.cs" />
</Folder>
<Folder Name="/Solution Items/src/Shared/Throw/">
<File Path="src/Shared/Throw/README.md" />
<File Path="src/Shared/Throw/Throw.cs" />
@@ -470,13 +538,13 @@
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
<Project Path="src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
<Project Path="src/Microsoft.Agents.AI.CosmosNoSql/Microsoft.Agents.AI.CosmosNoSql.csproj" />
<Project Path="src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj" />
<Project Path="src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj" />
<Project Path="src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj" />
<Project Path="src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj" />
<Project Path="src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj" />
<Project Path="src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<Project Path="src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
@@ -487,7 +555,7 @@
<Project Path="src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj" />
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Purview/Microsoft.Agents.AI.Purview.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.AzureAI/Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Generators/Microsoft.Agents.AI.Workflows.Generators.csproj" />
@@ -498,11 +566,10 @@
<Folder Name="/Tests/IntegrationTests/">
<Project Path="tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj" />
<Project Path="tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj" />
<Project Path="tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj" />
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj" />
<Project Path="tests/Foundry.IntegrationTests/Foundry.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests.csproj" />
@@ -513,17 +580,17 @@
<Project Path="tests/OpenAIResponse.IntegrationTests/OpenAIResponse.IntegrationTests.csproj" />
</Folder>
<Folder Name="/Tests/UnitTests/">
<Project Path="tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/Aspire.Hosting.AgentFramework.DevUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.UnitTests/Microsoft.Agents.AI.AzureAI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests.csproj" />
+6 -4
View File
@@ -8,13 +8,14 @@
"src\\Microsoft.Agents.AI.Anthropic\\Microsoft.Agents.AI.Anthropic.csproj",
"src\\Microsoft.Agents.AI.GitHub.Copilot\\Microsoft.Agents.AI.GitHub.Copilot.csproj",
"src\\Microsoft.Agents.AI.AzureAI.Persistent\\Microsoft.Agents.AI.AzureAI.Persistent.csproj",
"src\\Microsoft.Agents.AI.AzureAI\\Microsoft.Agents.AI.AzureAI.csproj",
"src\\Microsoft.Agents.AI.Foundry\\Microsoft.Agents.AI.Foundry.csproj",
"src\\Microsoft.Agents.AI.Foundry.Hosting\\Microsoft.Agents.AI.Foundry.Hosting.csproj",
"src\\Microsoft.Agents.AI.CopilotStudio\\Microsoft.Agents.AI.CopilotStudio.csproj",
"src\\Microsoft.Agents.AI.CosmosNoSql\\Microsoft.Agents.AI.CosmosNoSql.csproj",
"src\\Microsoft.Agents.AI.Declarative\\Microsoft.Agents.AI.Declarative.csproj",
"src\\Microsoft.Agents.AI.DevUI\\Microsoft.Agents.AI.DevUI.csproj",
"src\\Microsoft.Agents.AI.DurableTask\\Microsoft.Agents.AI.DurableTask.csproj",
"src\\Microsoft.Agents.AI.FoundryMemory\\Microsoft.Agents.AI.FoundryMemory.csproj",
"src\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj",
"src\\Microsoft.Agents.AI.Hosting.A2A\\Microsoft.Agents.AI.Hosting.A2A.csproj",
"src\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj",
@@ -24,11 +25,12 @@
"src\\Microsoft.Agents.AI.Mem0\\Microsoft.Agents.AI.Mem0.csproj",
"src\\Microsoft.Agents.AI.OpenAI\\Microsoft.Agents.AI.OpenAI.csproj",
"src\\Microsoft.Agents.AI.Purview\\Microsoft.Agents.AI.Purview.csproj",
"src\\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj",
"src\\Microsoft.Agents.AI.Workflows.Declarative.Foundry\\Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj",
"src\\Microsoft.Agents.AI.Workflows.Declarative\\Microsoft.Agents.AI.Workflows.Declarative.csproj",
"src\\Microsoft.Agents.AI.Workflows.Generators\\Microsoft.Agents.AI.Workflows.Generators.csproj",
"src\\Microsoft.Agents.AI.Workflows\\Microsoft.Agents.AI.Workflows.csproj",
"src\\Microsoft.Agents.AI\\Microsoft.Agents.AI.csproj"
"src\\Microsoft.Agents.AI\\Microsoft.Agents.AI.csproj",
"src\\Aspire.Hosting.AgentFramework.DevUI\\Aspire.Hosting.AgentFramework.DevUI.csproj"
]
}
}
+3
View File
@@ -29,4 +29,7 @@
<ItemGroup Condition="'$(InjectSharedDiagnosticIds)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\DiagnosticIds\*.cs" LinkBase="Shared\DiagnosticIds" />
</ItemGroup>
<ItemGroup Condition="'$(InjectSharedRedaction)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Redaction\*.cs" LinkBase="Shared\Redaction" />
</ItemGroup>
</Project>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,95 @@
// Copyright (c) Microsoft. All rights reserved.
namespace VerifySamples;
/// <summary>
/// Thread-safe console output with sample-name prefixes and colored status.
/// </summary>
internal sealed class ConsoleReporter
{
private readonly object _lock = new();
/// <summary>
/// Writes a complete prefixed line atomically to the console.
/// </summary>
public void WriteLineWithPrefix(string sampleName, string message, ConsoleColor? color = null)
{
lock (this._lock)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write($"[{sampleName}] ");
if (color.HasValue)
{
Console.ForegroundColor = color.Value;
}
else
{
Console.ResetColor();
}
Console.WriteLine(message);
Console.ResetColor();
}
}
/// <summary>
/// Prints the final summary table and elapsed time to the console.
/// </summary>
public void PrintSummary(
IReadOnlyList<VerificationResult> orderedResults,
IReadOnlyList<(string Name, string Reason)> skipped,
TimeSpan elapsed)
{
var passCount = orderedResults.Count(r => r.Passed);
var failCount = orderedResults.Count(r => !r.Passed);
Console.WriteLine();
Console.WriteLine(new string('─', 60));
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("SUMMARY");
Console.ResetColor();
foreach (var result in orderedResults)
{
Console.ForegroundColor = result.Passed ? ConsoleColor.Green : ConsoleColor.Red;
Console.Write(result.Passed ? " ✓ " : " ✗ ");
Console.ResetColor();
Console.WriteLine($"{result.SampleName}: {result.Summary}");
}
foreach (var (name, reason) in skipped)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write(" ○ ");
Console.ResetColor();
Console.WriteLine($"{name}: Skipped — {reason}");
}
Console.WriteLine();
Console.Write("Results: ");
Console.ForegroundColor = ConsoleColor.Green;
Console.Write($"{passCount} passed");
Console.ResetColor();
if (failCount > 0)
{
Console.Write(", ");
Console.ForegroundColor = ConsoleColor.Red;
Console.Write($"{failCount} failed");
Console.ResetColor();
}
if (skipped.Count > 0)
{
Console.Write(", ");
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write($"{skipped.Count} skipped");
Console.ResetColor();
}
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($"Elapsed: {elapsed.Hours:D2}:{elapsed.Minutes:D2}:{elapsed.Seconds:D2}");
Console.ResetColor();
}
}
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text;
namespace VerifySamples;
/// <summary>
/// Writes a CSV summary of sample verification results.
/// </summary>
internal static class CsvResultWriter
{
/// <summary>
/// Writes the results to a CSV file at the specified path.
/// </summary>
public static async Task WriteAsync(
string path,
IReadOnlyList<VerificationResult> orderedResults,
IReadOnlyList<(string Name, string Reason)> skipped,
IReadOnlyList<SampleDefinition> samples)
{
var pathLookup = samples.ToDictionary(s => s.Name, s => s.ProjectPath);
var sb = new StringBuilder();
sb.AppendLine("Sample,ProjectPath,Status,FailedChecks,Failures");
foreach (var result in orderedResults)
{
var status = result.Passed ? "PASSED" : "FAILED";
var failedChecks = result.Failures.Count;
var failures = string.Join("; ", result.Failures);
pathLookup.TryGetValue(result.SampleName, out var projectPath);
sb.AppendLine($"{CsvEscape(result.SampleName)},{CsvEscape(projectPath ?? "")},{status},{failedChecks},{CsvEscape(failures)}");
}
foreach (var (name, reason) in skipped)
{
pathLookup.TryGetValue(name, out var projectPath);
sb.AppendLine($"{CsvEscape(name)},{CsvEscape(projectPath ?? "")},SKIPPED,0,{CsvEscape(reason)}");
}
await File.WriteAllTextAsync(path, sb.ToString());
}
/// <summary>
/// Escapes a value for CSV: wraps in quotes if it contains commas, quotes, or newlines.
/// </summary>
private static string CsvEscape(string value)
{
if (value.Contains('"') || value.Contains(',') || value.Contains('\n') || value.Contains('\r'))
{
return $"\"{value.Replace("\"", "\"\"")}\"";
}
return value;
}
}
@@ -0,0 +1,105 @@
// Copyright (c) Microsoft. All rights reserved.
namespace VerifySamples;
/// <summary>
/// Defines the expected behavior for each sample in 01-get-started.
/// </summary>
internal static class GetStartedSamples
{
public static IReadOnlyList<SampleDefinition> All { get; } =
[
new SampleDefinition
{
Name = "05_first_workflow",
ProjectPath = "samples/01-get-started/05_first_workflow",
RequiredEnvironmentVariables = [],
IsDeterministic = true,
MustContain =
[
"UppercaseExecutor: HELLO, WORLD!",
"ReverseTextExecutor: !DLROW ,OLLEH",
],
},
new SampleDefinition
{
Name = "01_hello_agent",
ProjectPath = "samples/01-get-started/01_hello_agent",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
ExpectedOutputDescription =
[
"The output should contain a joke about a pirate.",
"There should be two separate joke responses — one from a non-streaming call and one from a streaming call.",
"The output should not contain error messages or stack traces.",
],
},
new SampleDefinition
{
Name = "02_add_tools",
ProjectPath = "samples/01-get-started/02_add_tools",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
MustContain = [],
ExpectedOutputDescription =
[
"The output should contain information about the weather in Amsterdam.",
"The response should mention that it is cloudy with a high of 15°C (or equivalent), since this comes from a tool that returns a canned response.",
"There should be two responses — one from a non-streaming call and one from a streaming call.",
"The output should not contain error messages or stack traces.",
],
},
new SampleDefinition
{
Name = "03_multi_turn",
ProjectPath = "samples/01-get-started/03_multi_turn",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
ExpectedOutputDescription =
[
"The output should contain a joke about a pirate.",
"After the initial joke, there should be a modified version that includes emojis and is told in the voice of a pirate's parrot.",
"The pattern repeats: first a non-streaming pirate joke + parrot version, then a streaming pirate joke + parrot version.",
"The output should not contain error messages or stack traces.",
],
},
new SampleDefinition
{
Name = "04_memory",
ProjectPath = "samples/01-get-started/04_memory",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
MustContain =
[
">> Use session with blank memory",
">> Use deserialized session with previously created memories",
">> Read memories using memory component",
"MEMORY - User Name:",
"MEMORY - User Age:",
">> Use new session with previously created memories",
],
ExpectedOutputDescription =
[
"In the 'Use session with blank memory' section, the agent should respond to the user's messages. It may ask for the user's name or age if not yet known.",
"In the 'Use deserialized session with previously created memories' section, the agent should correctly recall that the user's name is Ruaidhrí and age is 20.",
"The 'MEMORY - User Name:' line should show 'Ruaidhrí' (or a close transliteration).",
"The 'MEMORY - User Age:' line should show '20'.",
"In the 'Use new session with previously created memories' section, the agent should know the user's name and age from the transferred memory.",
"The output should not contain error messages or stack traces.",
],
},
new SampleDefinition
{
Name = "06_host_your_agent",
ProjectPath = "samples/01-get-started/06_host_your_agent",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
SkipReason = "Requires Azure Functions Core Tools runtime and starts a web server.",
},
];
}
+153
View File
@@ -0,0 +1,153 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text;
namespace VerifySamples;
/// <summary>
/// Incrementally writes a sequential (non-interleaved) log file, appending after each sample completes.
/// Thread-safe: multiple parallel tasks may call write methods concurrently.
/// </summary>
internal sealed class LogFileWriter : IDisposable
{
private readonly string _path;
private readonly SemaphoreSlim _writeLock = new(1, 1);
public LogFileWriter(string path)
{
this._path = path;
}
/// <inheritdoc />
public void Dispose()
{
this._writeLock.Dispose();
}
/// <summary>
/// Writes the log file header. Call once at the start of the run.
/// </summary>
public async Task WriteHeaderAsync()
{
var sb = new StringBuilder();
sb.AppendLine($"Sample Verification Log — {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
sb.AppendLine(new string('═', 72));
sb.AppendLine();
await File.WriteAllTextAsync(this._path, sb.ToString());
}
/// <summary>
/// Appends a skipped-sample entry to the log file.
/// </summary>
public async Task WriteSkippedAsync(string name, string reason)
{
var sb = new StringBuilder();
sb.AppendLine($"── {name} ──");
sb.AppendLine($"Status: SKIPPED — {reason}");
sb.AppendLine();
await this.AppendAsync(sb.ToString());
}
/// <summary>
/// Appends a completed sample's full output section to the log file.
/// </summary>
public async Task WriteSampleResultAsync(VerificationResult result)
{
var sb = new StringBuilder();
sb.AppendLine(new string('─', 72));
sb.AppendLine($"── {result.SampleName} ──");
sb.AppendLine($"Status: {(result.Passed ? "PASSED" : "FAILED")}");
sb.AppendLine();
foreach (var line in result.LogLines)
{
sb.AppendLine(line);
}
sb.AppendLine();
if (!string.IsNullOrWhiteSpace(result.Stdout))
{
sb.AppendLine("--- stdout ---");
sb.AppendLine(result.Stdout.TrimEnd());
sb.AppendLine("--- end stdout ---");
sb.AppendLine();
}
if (!string.IsNullOrWhiteSpace(result.Stderr))
{
sb.AppendLine("--- stderr ---");
sb.AppendLine(result.Stderr.TrimEnd());
sb.AppendLine("--- end stderr ---");
sb.AppendLine();
}
if (result.Failures.Count > 0)
{
sb.AppendLine("Failures:");
foreach (var failure in result.Failures)
{
sb.AppendLine($" ✗ {failure}");
}
sb.AppendLine();
}
if (result.AIReasoning is not null)
{
sb.AppendLine("AI Reasoning:");
sb.AppendLine(result.AIReasoning);
sb.AppendLine();
}
await this.AppendAsync(sb.ToString());
}
/// <summary>
/// Appends the final summary section and elapsed time to the log file.
/// </summary>
public async Task WriteSummaryAsync(
IReadOnlyList<VerificationResult> orderedResults,
IReadOnlyList<(string Name, string Reason)> skipped,
TimeSpan elapsed)
{
var passCount = orderedResults.Count(r => r.Passed);
var failCount = orderedResults.Count(r => !r.Passed);
var sb = new StringBuilder();
sb.AppendLine(new string('═', 72));
sb.AppendLine("SUMMARY");
sb.AppendLine();
foreach (var result in orderedResults)
{
sb.AppendLine($" {(result.Passed ? "" : "")} {result.SampleName}: {result.Summary}");
}
foreach (var (name, reason) in skipped)
{
sb.AppendLine($" ○ {name}: Skipped — {reason}");
}
sb.AppendLine();
sb.AppendLine($"Results: {passCount} passed{(failCount > 0 ? $", {failCount} failed" : "")}{(skipped.Count > 0 ? $", {skipped.Count} skipped" : "")}");
sb.AppendLine($"Elapsed: {elapsed.Hours:D2}:{elapsed.Minutes:D2}:{elapsed.Seconds:D2}");
await this.AppendAsync(sb.ToString());
}
private async Task AppendAsync(string text)
{
await this._writeLock.WaitAsync();
try
{
await File.AppendAllTextAsync(this._path, text);
}
finally
{
this._writeLock.Release();
}
}
}
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text;
namespace VerifySamples;
/// <summary>
/// Writes a Markdown summary of sample verification results.
/// </summary>
internal static class MarkdownResultWriter
{
/// <summary>
/// Writes the results to a Markdown file at the specified path.
/// </summary>
public static async Task WriteAsync(
string path,
IReadOnlyList<VerificationResult> orderedResults,
IReadOnlyList<(string Name, string Reason)> skipped,
TimeSpan elapsed)
{
var passCount = orderedResults.Count(r => r.Passed);
var failCount = orderedResults.Count(r => !r.Passed);
var sb = new StringBuilder();
sb.AppendLine("# Sample Verification Results");
sb.AppendLine();
sb.AppendLine($"**{passCount} passed, {failCount} failed, {skipped.Count} skipped** | Elapsed: {elapsed.Hours:D2}:{elapsed.Minutes:D2}:{elapsed.Seconds:D2}");
sb.AppendLine();
// Results table
sb.AppendLine("| Sample | Status | Failed Checks | Failures |");
sb.AppendLine("|--------|--------|---------------|----------|");
foreach (var result in orderedResults)
{
var status = result.Passed ? "✅ PASSED" : "❌ FAILED";
var failedChecks = result.Failures.Count;
var failures = MdEscape(string.Join("; ", result.Failures));
sb.AppendLine($"| {MdEscape(result.SampleName)} | {status} | {failedChecks} | {failures} |");
}
foreach (var (name, reason) in skipped)
{
sb.AppendLine($"| {MdEscape(name)} | ⏭️ SKIPPED | 0 | {MdEscape(reason)} |");
}
// Collapsible AI reasoning details for failures
var failures2 = orderedResults.Where(r => !r.Passed && !string.IsNullOrEmpty(r.AIReasoning)).ToList();
if (failures2.Count > 0)
{
sb.AppendLine();
sb.AppendLine("## Failure Details");
sb.AppendLine();
foreach (var result in failures2)
{
sb.AppendLine($"<details><summary><strong>{HtmlEscape(result.SampleName)}</strong></summary>");
sb.AppendLine();
if (result.Failures.Count > 0)
{
foreach (var failure in result.Failures)
{
sb.AppendLine($"- {MdEscape(failure)}");
}
sb.AppendLine();
}
sb.AppendLine("**AI Reasoning:**");
sb.AppendLine();
sb.AppendLine("```");
sb.AppendLine(result.AIReasoning);
sb.AppendLine("```");
sb.AppendLine();
sb.AppendLine("</details>");
sb.AppendLine();
}
}
await File.WriteAllTextAsync(path, sb.ToString());
}
/// <summary>
/// Escapes pipe characters and newlines for use inside Markdown table cells.
/// </summary>
private static string MdEscape(string value)
{
return value.Replace("|", "\\|").Replace("\n", " ").Replace("\r", "");
}
/// <summary>
/// Escapes HTML special characters for use inside HTML tags.
/// </summary>
private static string HtmlEscape(string value)
{
return value.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;");
}
}
+109
View File
@@ -0,0 +1,109 @@
// Copyright (c) Microsoft. All rights reserved.
// This tool runs the 01-get-started, 02-agents, and 03-workflows samples and verifies their output.
// Deterministic samples are verified with exact string matching.
// Non-deterministic (LLM) samples are verified using an agent-framework agent.
//
// Usage:
// dotnet run # Run all samples
// dotnet run -- 01_hello_agent 05_first_workflow # Run specific samples by name
// dotnet run -- --category 01-get-started # Run the 01-get-started category
// dotnet run -- --category 02-agents # Run the 02-agents category
// dotnet run -- --category 03-workflows # Run the 03-workflows category
// dotnet run -- --parallel 16 # Run up to 16 samples concurrently
// dotnet run -- --log results.log # Write sequential log to file
// dotnet run -- --csv results.csv # Write CSV summary to file
// dotnet run -- --md results.md # Write Markdown summary to file
// dotnet run -- --build # Build samples during run (default: --no-build)
// Note: By default, this tool expects sample build outputs to already exist.
// Pre-build the solution before running, or pass --build to avoid missing build output failures.
//
// Required environment variables (for AI-powered samples):
// AZURE_OPENAI_ENDPOINT
// AZURE_OPENAI_DEPLOYMENT_NAME (optional, defaults to gpt-5-mini)
using System.Diagnostics;
using Azure.AI.OpenAI;
using Azure.Identity;
using VerifySamples;
var options = VerifyOptions.Parse(args);
if (options is null)
{
return 1;
}
var stopwatch = Stopwatch.StartNew();
// Resolve the dotnet/ root directory (verify-samples is at dotnet/eng/verify-samples/)
var dotnetRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
if (!File.Exists(Path.Combine(dotnetRoot, "agent-framework-dotnet.slnx")))
{
dotnetRoot = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", ".."));
}
// Set up the AI verifier
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5-mini";
OpenAI.Chat.ChatClient? chatClient = null;
if (!string.IsNullOrEmpty(endpoint))
{
chatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetChatClient(deploymentName);
}
// Set up optional log file writer
LogFileWriter? logWriter = null;
if (options.LogFilePath is not null)
{
logWriter = new LogFileWriter(options.LogFilePath);
await logWriter.WriteHeaderAsync();
}
try
{
// Run all samples
var reporter = new ConsoleReporter();
var verifier = new SampleVerifier(chatClient);
var orchestrator = new VerificationOrchestrator(verifier, reporter, dotnetRoot, TimeSpan.FromMinutes(3), logWriter, buildSamples: options.BuildSamples);
var run = await orchestrator.RunAllAsync(options.Samples, options.MaxParallelism);
stopwatch.Stop();
// Print summary
var orderedResults = run.SampleOrder
.Where(run.Results.ContainsKey)
.Select(name => run.Results[name])
.ToList();
reporter.PrintSummary(orderedResults, run.Skipped, stopwatch.Elapsed);
// Write log file summary
if (logWriter is not null)
{
await logWriter.WriteSummaryAsync(orderedResults, run.Skipped, stopwatch.Elapsed);
Console.WriteLine($"Log written to: {options.LogFilePath}");
}
// Write CSV summary
if (options.CsvFilePath is not null)
{
await CsvResultWriter.WriteAsync(options.CsvFilePath, orderedResults, run.Skipped, options.Samples);
Console.WriteLine($"CSV written to: {options.CsvFilePath}");
}
// Write Markdown summary
if (options.MarkdownFilePath is not null)
{
await MarkdownResultWriter.WriteAsync(options.MarkdownFilePath, orderedResults, run.Skipped, stopwatch.Elapsed);
Console.WriteLine($"Markdown written to: {options.MarkdownFilePath}");
}
return orderedResults.Any(r => !r.Passed) ? 1 : 0;
}
finally
{
logWriter?.Dispose();
}
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft. All rights reserved.
namespace VerifySamples;
/// <summary>
/// Describes a sample to verify, including its expected output.
/// </summary>
internal sealed class SampleDefinition
{
/// <summary>
/// Display name for the sample (e.g., "01_hello_agent").
/// </summary>
public required string Name { get; init; }
/// <summary>
/// Relative path from the dotnet/ directory to the sample project directory.
/// </summary>
public required string ProjectPath { get; init; }
/// <summary>
/// Environment variables that the sample requires for a meaningful run.
/// The runner checks these before running and will skip the sample if any are unset,
/// recording a skip reason that indicates which required variables are missing.
/// </summary>
public string[] RequiredEnvironmentVariables { get; init; } = [];
/// <summary>
/// Environment variables that the sample can use but typically has fallbacks or defaults for.
/// If these are not set, the sample might prompt or behave interactively, which could cause
/// automated verification to hang. The runner checks these and skips the sample if they are unset
/// to avoid non-deterministic or blocking behavior in automated runs.
/// </summary>
public string[] OptionalEnvironmentVariables { get; init; } = [];
/// <summary>
/// If set, the sample is skipped with this reason.
/// Use only for structural reasons (e.g., web server, multi-process, needs external service).
/// Do NOT use for missing environment variables — those are checked dynamically.
/// </summary>
public string? SkipReason { get; init; }
/// <summary>
/// Substrings that must appear in stdout for the sample to pass.
/// Used for deterministic verification.
/// </summary>
public string[] MustContain { get; init; } = [];
/// <summary>
/// Substrings that must not appear in stdout for the sample to pass.
/// </summary>
public string[] MustNotContain { get; init; } = [];
/// <summary>
/// If true, <see cref="MustContain"/> entries cover the entire expected output —
/// no AI verification is needed.
/// </summary>
public bool IsDeterministic { get; init; }
/// <summary>
/// Natural-language description of what the sample output should look like.
/// Used by the AI verifier for non-deterministic samples.
/// Each entry describes one aspect of the expected output that should be verified.
/// </summary>
public string[] ExpectedOutputDescription { get; init; } = [];
/// <summary>
/// Sequence of stdin inputs to feed to the sample process.
/// Each entry is written as a line (followed by newline) to the process stdin.
/// A <c>null</c> entry inserts a delay without writing anything.
/// Inputs are sent with a short delay between each to allow the process to prompt.
/// </summary>
public string?[] Inputs { get; init; } = [];
/// <summary>
/// Delay in milliseconds between each input line. Default is 2000ms.
/// Increase for samples that need more time between prompts (e.g., LLM calls between inputs).
/// </summary>
public int InputDelayMs { get; init; } = 2000;
}
+141
View File
@@ -0,0 +1,141 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics;
namespace VerifySamples;
/// <summary>
/// Result of running a sample process.
/// </summary>
internal sealed record SampleRunResult(
string Stdout,
string Stderr,
int ExitCode,
TimeSpan Elapsed);
/// <summary>
/// Runs a sample project via <c>dotnet run</c> and captures its output.
/// </summary>
internal static class SampleRunner
{
/// <summary>
/// Runs <c>dotnet run --framework net10.0</c> in the given project directory.
/// When <paramref name="build"/> is false (the default), <c>--no-build</c> is passed
/// to skip building, assuming the project was pre-built.
/// </summary>
public static Task<SampleRunResult> RunAsync(
string projectPath,
TimeSpan timeout,
bool build = false,
CancellationToken cancellationToken = default)
=> RunAsync(projectPath, DotnetRunArgs(build), timeout, inputs: null, inputDelayMs: 0, cancellationToken: cancellationToken);
/// <summary>
/// Runs <c>dotnet run --framework net10.0</c> with stdin inputs.
/// When <paramref name="build"/> is false (the default), <c>--no-build</c> is passed
/// to skip building, assuming the project was pre-built.
/// </summary>
public static Task<SampleRunResult> RunAsync(
string projectPath,
TimeSpan timeout,
string?[]? inputs,
int inputDelayMs = 2000,
bool build = false,
CancellationToken cancellationToken = default)
=> RunAsync(projectPath, DotnetRunArgs(build), timeout, inputs, inputDelayMs, cancellationToken);
private static string DotnetRunArgs(bool build) =>
$"run {(build ? "" : "--no-build")} --framework net10.0";
/// <summary>
/// Runs an arbitrary <c>dotnet</c> command in the given working directory.
/// </summary>
public static async Task<SampleRunResult> RunAsync(
string workingDirectory,
string dotnetArgs,
TimeSpan timeout,
string?[]? inputs = null,
int inputDelayMs = 0,
CancellationToken cancellationToken = default)
{
var psi = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = dotnetArgs,
WorkingDirectory = workingDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = inputs is { Length: > 0 },
UseShellExecute = false,
CreateNoWindow = true,
};
var sw = Stopwatch.StartNew();
using var process = new Process { StartInfo = psi };
process.Start();
var stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
var stderrTask = process.StandardError.ReadToEndAsync(cancellationToken);
// Feed stdin inputs with delays if configured
if (inputs is { Length: > 0 })
{
_ = Task.Run(async () =>
{
try
{
foreach (var input in inputs)
{
await Task.Delay(inputDelayMs, cancellationToken);
if (input is not null)
{
await process.StandardInput.WriteLineAsync(input.AsMemory(), cancellationToken);
await process.StandardInput.FlushAsync(cancellationToken);
}
}
process.StandardInput.Close();
}
catch (Exception ex) when (ex is IOException or ObjectDisposedException or OperationCanceledException)
{
// Process may have exited before all inputs were sent
}
}, cancellationToken);
}
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(timeout);
try
{
await process.WaitForExitAsync(cts.Token);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
// Timeout — kill the process
try
{
process.Kill(entireProcessTree: true);
}
catch
{
// Best effort
}
sw.Stop();
return new SampleRunResult(
Stdout: await stdoutTask,
Stderr: $"TIMEOUT: Sample did not complete within {timeout.TotalSeconds}s.\n{await stderrTask}",
ExitCode: -1,
Elapsed: sw.Elapsed);
}
sw.Stop();
return new SampleRunResult(
Stdout: await stdoutTask,
Stderr: await stderrTask,
ExitCode: process.ExitCode,
Elapsed: sw.Elapsed);
}
}
+229
View File
@@ -0,0 +1,229 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
namespace VerifySamples;
/// <summary>
/// Verifies sample output using deterministic checks and an AI agent
/// for non-deterministic output validation.
/// </summary>
internal sealed class SampleVerifier
{
private readonly AIAgent? _verifierAgent;
/// <summary>
/// Creates a verifier. If <paramref name="chatClient"/> is provided,
/// AI-based verification is available for non-deterministic samples.
/// </summary>
public SampleVerifier(ChatClient? chatClient = null)
{
if (chatClient is not null)
{
this._verifierAgent = chatClient.AsAIAgent(
instructions: """
You are a test output verifier. You will be given:
1. The actual stdout output of a program
2. The stderr output (if any)
3. A list of expectations about what the output should contain or demonstrate
Your job is to determine whether the actual output satisfies each expectation.
Be reasonable the output comes from an LLM so exact wording won't match, but the
semantic intent should be clearly satisfied.
In your response, you MUST:
- Always provide ai_reasoning with a brief overall assessment.
- Always provide exactly one entry in expectation_results for each expectation,
in the same order as the input list.
- For each expectation_results entry, echo the expectation text in the expectation
field and explain your assessment in the detail field, citing evidence from the output.
""",
name: "OutputVerifier");
}
}
/// <summary>
/// Verifies the output of a sample run against its definition.
/// </summary>
public async Task<VerificationResult> VerifyAsync(SampleDefinition sample, SampleRunResult run)
{
var failures = new List<string>();
// 1. Exit code check
if (run.ExitCode != 0)
{
failures.Add($"Exit code was {run.ExitCode}, expected 0. Stderr: {Truncate(run.Stderr, 500)}");
}
// 2. Must-contain checks
foreach (var expected in sample.MustContain)
{
if (!run.Stdout.Contains(expected, StringComparison.Ordinal))
{
failures.Add($"Output missing expected substring: \"{expected}\"");
}
}
// 3. Must-not-contain checks
foreach (var unexpected in sample.MustNotContain)
{
if (run.Stdout.Contains(unexpected, StringComparison.Ordinal))
{
failures.Add($"Output contains unexpected substring: \"{unexpected}\"");
}
}
// 4. AI verification for non-deterministic samples
string? aiReasoning = null;
if (!sample.IsDeterministic && sample.ExpectedOutputDescription.Length > 0)
{
if (this._verifierAgent is null)
{
failures.Add("AI verification required but no AI agent configured (missing AZURE_OPENAI_ENDPOINT).");
}
else
{
var aiResult = await this.VerifyWithAIAsync(run.Stdout, run.Stderr, sample.ExpectedOutputDescription);
aiReasoning = aiResult.Reasoning;
foreach (var unmet in aiResult.UnmetExpectations)
{
failures.Add($"AI expectation not met: {unmet}");
}
}
}
bool passed = failures.Count == 0;
return new VerificationResult
{
SampleName = sample.Name,
Passed = passed,
Summary = passed ? "All checks passed" : $"{failures.Count} check(s) failed",
Failures = failures,
AIReasoning = aiReasoning,
};
}
private async Task<(string Reasoning, List<string> UnmetExpectations)> VerifyWithAIAsync(
string stdout,
string stderr,
string[] expectations)
{
var expectationList = string.Join("\n", expectations.Select((e, i) => $" {i + 1}. {e}"));
var stderrSection = string.IsNullOrWhiteSpace(stderr)
? ""
: $"""
Stderr output:
---
{Truncate(stderr, 2000)}
---
""";
var prompt = $"""
Actual program output:
---
{Truncate(stdout, 4000)}
---
{stderrSection}
Expectations to verify:
{expectationList}
Does the output satisfy all expectations?
""";
try
{
var response = await this._verifierAgent!.RunAsync<AIVerificationResponse>(prompt);
var result = response.Result;
if (result is null)
{
return ($"AI verification returned null result. Raw: {response.Text}", ["AI verification returned null result."]);
}
var reasoning = string.IsNullOrWhiteSpace(result.AIReasoning)
? "(no reasoning provided)"
: result.AIReasoning;
// Collect unmet expectations as individual failures
var unmet = new List<string>();
if (result.ExpectationResults is { Count: > 0 })
{
foreach (var er in result.ExpectationResults.Where(er => !er.Met))
{
var detail = string.IsNullOrWhiteSpace(er.Detail) ? er.Expectation : $"{er.Expectation} — {er.Detail}";
unmet.Add(detail ?? "Unknown expectation");
}
// If the model flagged overall failure but all individual expectations were met,
// still treat as failure using the overall reasoning.
if (unmet.Count == 0 && !result.Pass)
{
unmet.Add(reasoning);
}
}
else if (!result.Pass)
{
// Fallback: no per-expectation detail but overall pass is false
unmet.Add(reasoning);
}
return (reasoning, unmet);
}
catch (Exception ex)
{
return ($"AI verification error: {ex.Message}", [$"AI verification error: {ex.Message}"]);
}
}
private static string Truncate(string text, int maxLength)
=> text.Length <= maxLength ? text : text[..maxLength] + "... (truncated)";
}
/// <summary>
/// Structured response from the AI verification agent.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by JSON deserialization via RunAsync<T>.")]
internal sealed class AIVerificationResponse
{
/// <summary>Whether all expectations were met.</summary>
[JsonPropertyName("pass")]
public bool Pass { get; set; }
/// <summary>Brief explanation of the overall assessment.</summary>
[JsonPropertyName("ai_reasoning")]
[Description("Always required. Brief explanation of the overall assessment, covering all expectations.")]
public string AIReasoning { get; set; } = string.Empty;
/// <summary>Per-expectation results.</summary>
[JsonPropertyName("expectation_results")]
[Description("Always required. One entry per expectation, in the same order as the input list.")]
public List<ExpectationResult> ExpectationResults { get; set; } = [];
}
/// <summary>
/// Result for an individual expectation check.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by JSON deserialization via RunAsync<T>.")]
internal sealed class ExpectationResult
{
/// <summary>The expectation text that was evaluated.</summary>
[JsonPropertyName("expectation")]
[Description("Echo back the expectation text being evaluated.")]
public string Expectation { get; set; } = string.Empty;
/// <summary>Whether this expectation was met.</summary>
[JsonPropertyName("met")]
public bool Met { get; set; }
/// <summary>Detail about how the expectation was or was not met.</summary>
[JsonPropertyName("detail")]
[Description("Explain how the expectation was or was not met, citing specific evidence from the output.")]
public string Detail { get; set; } = string.Empty;
}
@@ -0,0 +1,200 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Concurrent;
namespace VerifySamples;
/// <summary>
/// Orchestrates sample verification: filters, runs in parallel, and collects results.
/// </summary>
internal sealed class VerificationOrchestrator
{
private readonly SampleVerifier _verifier;
private readonly ConsoleReporter _reporter;
private readonly LogFileWriter? _logWriter;
private readonly string _dotnetRoot;
private readonly TimeSpan _timeout;
private readonly bool _buildSamples;
public VerificationOrchestrator(
SampleVerifier verifier,
ConsoleReporter reporter,
string dotnetRoot,
TimeSpan timeout,
LogFileWriter? logWriter = null,
bool buildSamples = false)
{
this._verifier = verifier;
this._reporter = reporter;
this._logWriter = logWriter;
this._dotnetRoot = dotnetRoot;
this._timeout = timeout;
this._buildSamples = buildSamples;
}
/// <summary>
/// The result of running all samples through the orchestrator.
/// </summary>
internal sealed record RunAllResult(
ConcurrentDictionary<string, VerificationResult> Results,
List<(string Name, string Reason)> Skipped,
List<string> SampleOrder);
/// <summary>
/// Filters samples, runs the runnable ones in parallel, and returns all results.
/// </summary>
public async Task<RunAllResult> RunAllAsync(
IReadOnlyList<SampleDefinition> samples,
int maxParallelism)
{
var skipped = new List<(string Name, string Reason)>();
var runnableSamples = new List<SampleDefinition>();
var sampleOrder = new List<string>();
// Separate samples into skipped and runnable
foreach (var sample in samples)
{
sampleOrder.Add(sample.Name);
if (sample.SkipReason is not null)
{
skipped.Add((sample.Name, sample.SkipReason));
this._reporter.WriteLineWithPrefix(sample.Name, $"SKIPPED — {sample.SkipReason}", ConsoleColor.Yellow);
if (this._logWriter is not null)
{
await this._logWriter.WriteSkippedAsync(sample.Name, sample.SkipReason);
}
continue;
}
var missingRequired = sample.RequiredEnvironmentVariables
.Where(v => string.IsNullOrEmpty(Environment.GetEnvironmentVariable(v)))
.ToList();
var missingOptional = sample.OptionalEnvironmentVariables
.Where(v => string.IsNullOrEmpty(Environment.GetEnvironmentVariable(v)))
.ToList();
if (missingRequired.Count > 0 || missingOptional.Count > 0)
{
var reasons = new List<string>();
if (missingRequired.Count > 0)
{
reasons.Add($"Missing required: {string.Join(", ", missingRequired)}");
}
if (missingOptional.Count > 0)
{
reasons.Add($"Missing optional (would cause console prompt hang): {string.Join(", ", missingOptional)}");
}
var skipReason = string.Join("; ", reasons);
skipped.Add((sample.Name, skipReason));
this._reporter.WriteLineWithPrefix(sample.Name, $"SKIPPED — {skipReason}", ConsoleColor.Yellow);
if (this._logWriter is not null)
{
await this._logWriter.WriteSkippedAsync(sample.Name, skipReason);
}
continue;
}
runnableSamples.Add(sample);
}
// Run samples in parallel
var results = new ConcurrentDictionary<string, VerificationResult>();
var semaphore = new SemaphoreSlim(maxParallelism);
this._reporter.WriteLineWithPrefix(
"runner", $"Running {runnableSamples.Count} samples (max {maxParallelism} parallel)...");
try
{
var tasks = runnableSamples.Select(sample => this.RunSingleAsync(sample, results, semaphore)).ToArray();
await Task.WhenAll(tasks);
}
finally
{
semaphore.Dispose();
}
return new RunAllResult(results, skipped, sampleOrder);
}
private async Task RunSingleAsync(
SampleDefinition sample,
ConcurrentDictionary<string, VerificationResult> results,
SemaphoreSlim semaphore)
{
await semaphore.WaitAsync();
try
{
var log = new List<string>();
log.Add($"[{sample.Name}] Running...");
this._reporter.WriteLineWithPrefix(sample.Name, "Running...");
var projectPath = Path.Combine(this._dotnetRoot, sample.ProjectPath);
var run = sample.Inputs.Length > 0
? await SampleRunner.RunAsync(projectPath, this._timeout, sample.Inputs, sample.InputDelayMs, build: this._buildSamples)
: await SampleRunner.RunAsync(projectPath, this._timeout, build: this._buildSamples);
log.Add($"[{sample.Name}] Completed ({run.Elapsed.TotalSeconds:F1}s, exit={run.ExitCode})");
this._reporter.WriteLineWithPrefix(
sample.Name, $"Completed ({run.Elapsed.TotalSeconds:F1}s, exit={run.ExitCode}). Verifying...");
var result = await this._verifier.VerifyAsync(sample, run);
if (result.Passed)
{
log.Add($"[{sample.Name}] PASSED");
this._reporter.WriteLineWithPrefix(sample.Name, "PASSED", ConsoleColor.Green);
}
else
{
log.Add($"[{sample.Name}] FAILED");
this._reporter.WriteLineWithPrefix(sample.Name, "FAILED", ConsoleColor.Red);
foreach (var failure in result.Failures)
{
log.Add($"[{sample.Name}] ✗ {failure}");
this._reporter.WriteLineWithPrefix(sample.Name, $" ✗ {failure}", ConsoleColor.Red);
}
}
if (result.AIReasoning is not null)
{
log.Add($"[{sample.Name}] AI: {result.AIReasoning}");
this._reporter.WriteLineWithPrefix(
sample.Name, $" AI: {Truncate(result.AIReasoning, 300)}", ConsoleColor.DarkGray);
}
var verificationResult = new VerificationResult
{
SampleName = result.SampleName,
Passed = result.Passed,
Summary = result.Summary,
Failures = result.Failures,
AIReasoning = result.AIReasoning,
Stdout = run.Stdout,
Stderr = run.Stderr,
LogLines = log,
};
results[sample.Name] = verificationResult;
if (this._logWriter is not null)
{
await this._logWriter.WriteSampleResultAsync(verificationResult);
}
}
finally
{
semaphore.Release();
}
}
private static string Truncate(string text, int maxLength)
=> text.Length <= maxLength ? text : text[..maxLength] + "...";
}
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
namespace VerifySamples;
/// <summary>
/// The result of verifying a single sample.
/// </summary>
internal sealed class VerificationResult
{
public required string SampleName { get; init; }
public required bool Passed { get; init; }
public required string Summary { get; init; }
public List<string> Failures { get; init; } = [];
public string? AIReasoning { get; init; }
/// <summary>
/// The sample's stdout output, captured for log file output.
/// </summary>
public string? Stdout { get; init; }
/// <summary>
/// The sample's stderr output, captured for log file output.
/// </summary>
public string? Stderr { get; init; }
/// <summary>
/// Per-sample log lines, buffered during parallel execution
/// and written sequentially to the log file.
/// </summary>
public List<string> LogLines { get; init; } = [];
}
+151
View File
@@ -0,0 +1,151 @@
// Copyright (c) Microsoft. All rights reserved.
namespace VerifySamples;
/// <summary>
/// Parsed command-line options for the sample verification tool.
/// </summary>
internal sealed class VerifyOptions
{
/// <summary>
/// Maximum number of samples to run concurrently.
/// </summary>
public int MaxParallelism { get; init; } = 8;
/// <summary>
/// Path to write a CSV summary file, or <c>null</c> to skip.
/// </summary>
public string? CsvFilePath { get; init; }
/// <summary>
/// Path to write a Markdown summary file, or <c>null</c> to skip.
/// </summary>
public string? MarkdownFilePath { get; init; }
/// <summary>
/// Path to write a sequential log file, or <c>null</c> to skip.
/// </summary>
public string? LogFilePath { get; init; }
/// <summary>
/// When true, samples are built as part of <c>dotnet run</c>.
/// When false (the default), <c>--no-build</c> is passed, assuming a prior build step.
/// </summary>
public bool BuildSamples { get; init; }
/// <summary>
/// The filtered list of samples to process.
/// </summary>
public required IReadOnlyList<SampleDefinition> Samples { get; init; }
/// <summary>
/// All known sample set registries, keyed by category name.
/// </summary>
private static readonly Dictionary<string, IReadOnlyList<SampleDefinition>> s_sampleSets =
new(StringComparer.OrdinalIgnoreCase)
{
["01-get-started"] = GetStartedSamples.All,
["02-agents"] = AgentsSamples.All,
["03-workflows"] = WorkflowSamples.All,
};
/// <summary>
/// Parses command-line arguments and resolves the sample list.
/// Returns <c>null</c> and writes to stderr if the arguments are invalid.
/// </summary>
public static VerifyOptions? Parse(string[] args)
{
var argList = args.ToList();
var categoryFilter = ExtractArg(argList, "--category");
var logFilePath = ExtractArg(argList, "--log");
var csvFilePath = ExtractArg(argList, "--csv");
var markdownFilePath = ExtractArg(argList, "--md");
var buildSamples = ExtractFlag(argList, "--build");
int maxParallelism = 8;
var parallelArg = ExtractArg(argList, "--parallel");
if (parallelArg is not null && int.TryParse(parallelArg, out var p) && p > 0)
{
maxParallelism = p;
}
HashSet<string>? nameFilter = null;
if (argList.Count > 0)
{
nameFilter = argList.ToHashSet(StringComparer.OrdinalIgnoreCase);
}
// Build the sample list
IReadOnlyList<SampleDefinition> samples;
if (categoryFilter is not null)
{
if (!s_sampleSets.TryGetValue(categoryFilter, out var categoryList))
{
Console.Error.WriteLine(
$"Unknown category '{categoryFilter}'. Available: {string.Join(", ", s_sampleSets.Keys)}");
return null;
}
samples = categoryList;
}
else
{
samples = s_sampleSets.Values.SelectMany(s => s).ToList();
}
if (nameFilter is not null)
{
samples = samples.Where(s => nameFilter.Contains(s.Name)).ToList();
}
if (samples.Count == 0)
{
var allNames = s_sampleSets.Values.SelectMany(s => s).Select(s => s.Name);
Console.Error.WriteLine($"No matching samples found. Available: {string.Join(", ", allNames)}");
return null;
}
return new VerifyOptions
{
MaxParallelism = maxParallelism,
LogFilePath = logFilePath,
CsvFilePath = csvFilePath,
MarkdownFilePath = markdownFilePath,
BuildSamples = buildSamples,
Samples = samples,
};
}
private static string? ExtractArg(List<string> list, string flag)
{
var idx = list.IndexOf(flag);
if (idx < 0)
{
return null;
}
if (idx + 1 >= list.Count)
{
Console.Error.WriteLine($"Missing value for {flag}.");
list.RemoveAt(idx);
return null;
}
var value = list[idx + 1];
list.RemoveRange(idx, 2);
return value;
}
private static bool ExtractFlag(List<string> list, string flag)
{
var idx = list.IndexOf(flag);
if (idx < 0)
{
return false;
}
list.RemoveAt(idx);
return true;
}
}
@@ -0,0 +1,525 @@
// Copyright (c) Microsoft. All rights reserved.
namespace VerifySamples;
/// <summary>
/// Defines the expected behavior for each sample in 03-workflows.
/// </summary>
internal static class WorkflowSamples
{
public static IReadOnlyList<SampleDefinition> All { get; } =
[
// ───────────────────────────────────────────────────────────────────
// _StartHere
// ───────────────────────────────────────────────────────────────────
new SampleDefinition
{
Name = "Workflow_StartHere_01_Streaming",
ProjectPath = "samples/03-workflows/_StartHere/01_Streaming",
RequiredEnvironmentVariables = [],
IsDeterministic = true,
MustContain =
[
"UppercaseExecutor: HELLO, WORLD!",
"ReverseTextExecutor: !DLROW ,OLLEH",
],
},
new SampleDefinition
{
Name = "Workflow_StartHere_02_AgentsInWorkflows",
ProjectPath = "samples/03-workflows/_StartHere/02_AgentsInWorkflows",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
ExpectedOutputDescription =
[
"The output should show agent responses from a translation workflow.",
"The output should not contain error messages or stack traces.",
],
},
new SampleDefinition
{
Name = "Workflow_StartHere_03_AgentWorkflowPatterns",
ProjectPath = "samples/03-workflows/_StartHere/03_AgentWorkflowPatterns",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
Inputs = ["sequential"],
InputDelayMs = 3000,
ExpectedOutputDescription =
[
"The output should show a sequential workflow pattern with multiple agents executing tasks in order.",
"The output should not contain error messages or stack traces.",
],
},
new SampleDefinition
{
Name = "Workflow_StartHere_04_MultiModelService",
ProjectPath = "samples/03-workflows/_StartHere/04_MultiModelService",
RequiredEnvironmentVariables = ["BEDROCK_ACCESS_KEY", "BEDROCK_SECRET_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
SkipReason = "Requires multiple external provider API keys (Bedrock, Anthropic, OpenAI).",
},
new SampleDefinition
{
Name = "Workflow_StartHere_05_SubWorkflows",
ProjectPath = "samples/03-workflows/_StartHere/05_SubWorkflows",
RequiredEnvironmentVariables = [],
IsDeterministic = true,
MustContain =
[
"=== Sub-Workflow Demonstration ===",
"Final Output:",
"=== Main Workflow Completed ===",
"Sample Complete: Workflows can be composed hierarchically using sub-workflows",
],
},
new SampleDefinition
{
Name = "Workflow_StartHere_06_MixedWorkflowAgentsAndExecutors",
ProjectPath = "samples/03-workflows/_StartHere/06_MixedWorkflowAgentsAndExecutors",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
Inputs = ["What is 2 plus 2?"],
InputDelayMs = 3000,
ExpectedOutputDescription =
[
"The output should show agents and executors working together to process a user question.",
"The output should not contain error messages or stack traces.",
],
},
new SampleDefinition
{
Name = "Workflow_StartHere_07_WriterCriticWorkflow",
ProjectPath = "samples/03-workflows/_StartHere/07_WriterCriticWorkflow",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
MustContain = ["=== Writer-Critic Iteration Workflow ==="],
ExpectedOutputDescription =
[
"The output should show a writer-critic iteration workflow with writer and critic sections.",
"The critic should either approve or request revisions.",
"The output should not contain error messages or stack traces.",
],
},
// ───────────────────────────────────────────────────────────────────
// Agents
// ───────────────────────────────────────────────────────────────────
new SampleDefinition
{
Name = "Workflow_Agents_CustomAgentExecutors",
ProjectPath = "samples/03-workflows/Agents/CustomAgentExecutors",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
ExpectedOutputDescription =
[
"The output should show custom workflow events including slogan generation and feedback.",
"The output should not contain error messages or stack traces.",
],
},
new SampleDefinition
{
Name = "Workflow_Agents_FoundryAgent",
ProjectPath = "samples/03-workflows/Agents/FoundryAgent",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
SkipReason = "Requires Azure AI Foundry project endpoint.",
},
new SampleDefinition
{
Name = "Workflow_Agents_GroupChatToolApproval",
ProjectPath = "samples/03-workflows/Agents/GroupChatToolApproval",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
MustContain = ["Starting group chat workflow for software deployment..."],
ExpectedOutputDescription =
[
"The output should show a group chat workflow with QA and DevOps agents for software deployment.",
"There should be approval requests for tool calls.",
"The workflow should show interaction between QA and DevOps agents toward deployment.",
"The output should not contain error messages or stack traces.",
],
},
new SampleDefinition
{
Name = "Workflow_Agents_WorkflowAsAnAgent",
ProjectPath = "samples/03-workflows/Agents/WorkflowAsAnAgent",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
Inputs = ["hello", "exit"],
InputDelayMs = 5000,
ExpectedOutputDescription =
[
"The output should show a conversational workflow responding to the user's hello message.",
"The output should not contain error messages or stack traces.",
],
},
// ───────────────────────────────────────────────────────────────────
// Checkpoint
// ───────────────────────────────────────────────────────────────────
new SampleDefinition
{
Name = "Workflow_Checkpoint_CheckpointAndRehydrate",
ProjectPath = "samples/03-workflows/Checkpoint/CheckpointAndRehydrate",
RequiredEnvironmentVariables = [],
IsDeterministic = true,
MustContain =
[
"Workflow completed with result:",
"Number of checkpoints created:",
"Hydrating a new workflow instance from the 6th checkpoint.",
],
},
new SampleDefinition
{
Name = "Workflow_Checkpoint_CheckpointAndResume",
ProjectPath = "samples/03-workflows/Checkpoint/CheckpointAndResume",
RequiredEnvironmentVariables = [],
IsDeterministic = true,
MustContain =
[
"Workflow completed with result:",
"Number of checkpoints created:",
"Restoring from the 6th checkpoint.",
],
},
new SampleDefinition
{
Name = "Workflow_Checkpoint_CheckpointWithHumanInTheLoop",
ProjectPath = "samples/03-workflows/Checkpoint/CheckpointWithHumanInTheLoop",
RequiredEnvironmentVariables = [],
Inputs = ["50", "25", "40", "45", "42", "50", "25", "40", "45", "42"],
InputDelayMs = 1000,
MustContain = ["found in"],
ExpectedOutputDescription =
[
"The output should show a number guessing game with higher/lower hints that eventually reaches the correct number.",
"The output should demonstrate checkpoint save and restore behavior.",
],
},
// ───────────────────────────────────────────────────────────────────
// Concurrent
// ───────────────────────────────────────────────────────────────────
new SampleDefinition
{
Name = "Workflow_Concurrent_Concurrent",
ProjectPath = "samples/03-workflows/Concurrent/Concurrent",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
ExpectedOutputDescription =
[
"The output should show results from concurrent agent processing.",
"The output should not contain error messages or stack traces.",
],
},
new SampleDefinition
{
Name = "Workflow_Concurrent_MapReduce",
ProjectPath = "samples/03-workflows/Concurrent/MapReduce",
RequiredEnvironmentVariables = [],
MustContain =
[
"=== RUNNING WORKFLOW ===",
],
},
// ───────────────────────────────────────────────────────────────────
// ConditionalEdges
// ───────────────────────────────────────────────────────────────────
new SampleDefinition
{
Name = "Workflow_ConditionalEdges_01_EdgeCondition",
ProjectPath = "samples/03-workflows/ConditionalEdges/01_EdgeCondition",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
ExpectedOutputDescription =
[
"The output should show an email being classified as spam or not spam and processed accordingly.",
"The output should not contain error messages or stack traces.",
],
},
new SampleDefinition
{
Name = "Workflow_ConditionalEdges_02_SwitchCase",
ProjectPath = "samples/03-workflows/ConditionalEdges/02_SwitchCase",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
ExpectedOutputDescription =
[
"The output should show an ambiguous email being classified as spam, not spam, or uncertain.",
"The output should not contain error messages or stack traces.",
],
},
new SampleDefinition
{
Name = "Workflow_ConditionalEdges_03_MultiSelection",
ProjectPath = "samples/03-workflows/ConditionalEdges/03_MultiSelection",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
ExpectedOutputDescription =
[
"The output should show an email being classified and potentially routed to multiple handlers.",
"The output should not contain error messages or stack traces.",
],
},
// ───────────────────────────────────────────────────────────────────
// HumanInTheLoop
// ───────────────────────────────────────────────────────────────────
new SampleDefinition
{
Name = "Workflow_HumanInTheLoop_Basic",
ProjectPath = "samples/03-workflows/HumanInTheLoop/HumanInTheLoopBasic",
RequiredEnvironmentVariables = [],
Inputs = ["50", "25", "40", "45", "42"],
InputDelayMs = 1000,
MustContain = ["found in"],
ExpectedOutputDescription =
[
"The output should show a number guessing game with higher/lower hints that eventually reaches the correct number 42.",
],
},
// ───────────────────────────────────────────────────────────────────
// Loop
// ───────────────────────────────────────────────────────────────────
new SampleDefinition
{
Name = "Workflow_Loop",
ProjectPath = "samples/03-workflows/Loop",
RequiredEnvironmentVariables = [],
MustContain = ["Result:"],
},
// ───────────────────────────────────────────────────────────────────
// SharedStates
// ───────────────────────────────────────────────────────────────────
new SampleDefinition
{
Name = "Workflow_SharedStates",
ProjectPath = "samples/03-workflows/SharedStates",
RequiredEnvironmentVariables = [],
IsDeterministic = true,
MustContain =
[
"Total Paragraphs:",
"Total Words:",
],
},
// ───────────────────────────────────────────────────────────────────
// Visualization
// ───────────────────────────────────────────────────────────────────
new SampleDefinition
{
Name = "Workflow_Visualization",
ProjectPath = "samples/03-workflows/Visualization",
RequiredEnvironmentVariables = [],
IsDeterministic = true,
MustContain =
[
"Generating workflow visualization...",
"Mermaid string:",
"DiGraph string:",
],
},
// ───────────────────────────────────────────────────────────────────
// Observability
// ───────────────────────────────────────────────────────────────────
new SampleDefinition
{
Name = "Workflow_Observability_ApplicationInsights",
ProjectPath = "samples/03-workflows/Observability/ApplicationInsights",
RequiredEnvironmentVariables = ["APPLICATIONINSIGHTS_CONNECTION_STRING"],
SkipReason = "Requires Application Insights connection string.",
},
new SampleDefinition
{
Name = "Workflow_Observability_AspireDashboard",
ProjectPath = "samples/03-workflows/Observability/AspireDashboard",
RequiredEnvironmentVariables = [],
SkipReason = "Requires Aspire Dashboard / OTLP endpoint.",
},
new SampleDefinition
{
Name = "Workflow_Observability_WorkflowAsAnAgent",
ProjectPath = "samples/03-workflows/Observability/WorkflowAsAnAgent",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
SkipReason = "Interactive console with ReadLine loop; requires OTLP endpoint.",
},
// ───────────────────────────────────────────────────────────────────
// Declarative
// ───────────────────────────────────────────────────────────────────
new SampleDefinition
{
Name = "Workflow_Declarative_ConfirmInput",
ProjectPath = "samples/03-workflows/Declarative/ConfirmInput",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
Inputs = ["hello", "hello"],
InputDelayMs = 8000,
ExpectedOutputDescription = ["The output should show a confirmation prompt and a user response."],
},
new SampleDefinition
{
Name = "Workflow_Declarative_CustomerSupport",
ProjectPath = "samples/03-workflows/Declarative/CustomerSupport",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
Inputs = ["My laptop won't start"],
InputDelayMs = 3000,
ExpectedOutputDescription = ["The output should show a customer support workflow processing a laptop issue, with agent responses providing troubleshooting or support."],
},
new SampleDefinition
{
Name = "Workflow_Declarative_DeepResearch",
ProjectPath = "samples/03-workflows/Declarative/DeepResearch",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
SkipReason = "Requires external weather API (wttr.in).",
},
new SampleDefinition
{
Name = "Workflow_Declarative_ExecuteCode",
ProjectPath = "samples/03-workflows/Declarative/ExecuteCode",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
Inputs = ["What is 12 * 34?"],
InputDelayMs = 5000,
ExpectedOutputDescription = ["The output should show a declarative workflow executing generated code, processing a math question and producing a result."],
},
new SampleDefinition
{
Name = "Workflow_Declarative_ExecuteWorkflow",
ProjectPath = "samples/03-workflows/Declarative/ExecuteWorkflow",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
SkipReason = "Requires a workflow file path as a CLI argument.",
},
new SampleDefinition
{
Name = "Workflow_Declarative_FunctionTools",
ProjectPath = "samples/03-workflows/Declarative/FunctionTools",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
Inputs = ["What are today's specials?", "EXIT"],
InputDelayMs = 8000,
ExpectedOutputDescription = ["The output should show a workflow calling function tools (e.g. a menu plugin) to answer a question about restaurant specials."],
},
new SampleDefinition
{
Name = "Workflow_Declarative_GenerateCode",
ProjectPath = "samples/03-workflows/Declarative/GenerateCode",
IsDeterministic = true,
MustContain = ["WORKFLOW: Parsing", "WORKFLOW: Defined"],
ExpectedOutputDescription = ["The output should show a YAML workflow being parsed and C# code being generated from it."],
},
new SampleDefinition
{
Name = "Workflow_Declarative_HostedWorkflow",
ProjectPath = "samples/03-workflows/Declarative/HostedWorkflow",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
SkipReason = "Hosts a persistent workflow server that does not exit.",
},
new SampleDefinition
{
Name = "Workflow_Declarative_InputArguments",
ProjectPath = "samples/03-workflows/Declarative/InputArguments",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
Inputs = ["I'd like to visit Seattle", "EXIT"],
InputDelayMs = 8000,
ExpectedOutputDescription = ["The output should show a workflow capturing location input and providing travel-related information about Seattle."],
},
new SampleDefinition
{
Name = "Workflow_Declarative_InvokeFunctionTool",
ProjectPath = "samples/03-workflows/Declarative/InvokeFunctionTool",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
Inputs = ["What's the soup of the day?", "EXIT"],
InputDelayMs = 8000,
ExpectedOutputDescription = ["The output should show a workflow invoking a function tool (e.g. a menu plugin) to answer a question about the soup of the day."],
},
new SampleDefinition
{
Name = "Workflow_Declarative_InvokeMcpTool",
ProjectPath = "samples/03-workflows/Declarative/InvokeMcpTool",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
Inputs = ["Search for .NET tutorials on Microsoft Learn"],
InputDelayMs = 3000,
ExpectedOutputDescription = ["The output should show a workflow using MCP tools to search Microsoft Learn documentation and provide a summary of results."],
},
new SampleDefinition
{
Name = "Workflow_Declarative_Marketing",
ProjectPath = "samples/03-workflows/Declarative/Marketing",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
Inputs = ["A smart water bottle that tracks hydration"],
InputDelayMs = 3000,
ExpectedOutputDescription = ["The output should show a marketing workflow generating content about a smart water bottle product."],
},
new SampleDefinition
{
Name = "Workflow_Declarative_StudentTeacher",
ProjectPath = "samples/03-workflows/Declarative/StudentTeacher",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
Inputs = ["What is 18 + 27?"],
InputDelayMs = 3000,
ExpectedOutputDescription = ["The output should show a student-teacher workflow where a student asks a math question and a teacher provides the answer."],
},
new SampleDefinition
{
Name = "Workflow_Declarative_ToolApproval",
ProjectPath = "samples/03-workflows/Declarative/ToolApproval",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
Inputs = ["Search for .NET tutorials", "EXIT"],
InputDelayMs = 8000,
ExpectedOutputDescription = ["The output should show a workflow using an MCP tool with approval to search Microsoft Learn, followed by an exit from the input loop."],
},
];
}
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<IsAotCompatible>false</IsAotCompatible>
<!-- This is a top-level console app; ConfigureAwait is unnecessary -->
<NoWarn>$(NoWarn);CA2007</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
+2 -2
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
@@ -9,4 +9,4 @@
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>
</configuration>
+12 -7
View File
@@ -1,18 +1,22 @@
<Project>
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.0.0</VersionPrefix>
<RCNumber>4</RCNumber>
<VersionPrefix>1.2.0</VersionPrefix>
<RCNumber>1</RCNumber>
<DateSuffix>260421</DateSuffix>
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260311.1</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260311.1</PackageVersion>
<GitTag>1.0.0-rc4</GitTag>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).$(DateSuffix).1</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.$(DateSuffix).1</PackageVersion>
<PackageVersion Condition="'$(IsReleased)' == 'true'">$(VersionPrefix)</PackageVersion>
<GitTag>1.2.0</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
<!-- Package validation. Baseline Version should be the latest version available on NuGet. -->
<PackageValidationBaselineVersion>0.0.1</PackageValidationBaselineVersion>
<PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion>
<!-- Enable validation for GA packages -->
<EnablePackageValidation Condition="'$(IsReleased)' == 'true'">true</EnablePackageValidation>
<!-- Validate assembly attributes only for Publish builds -->
<NoWarn Condition="'$(Configuration)' != 'Publish'">$(NoWarn);CP0003</NoWarn>
<!-- Do not validate reference assemblies -->
@@ -26,7 +30,8 @@
<!-- Report low, moderate, high and critical advisories -->
<NuGetAuditLevel>low</NuGetAuditLevel>
<!-- Default description and tags. Packages can override. -->
<Authors>Microsoft</Authors>
<Company>Microsoft</Company>
@@ -8,7 +8,7 @@ using Microsoft.Agents.AI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
@@ -11,7 +11,7 @@ using Microsoft.Extensions.AI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
[Description("Get the weather for a given location.")]
static string GetWeather([Description("The location to get the weather for.")] string location)
@@ -8,7 +8,7 @@ using Microsoft.Agents.AI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
@@ -16,7 +16,7 @@ using OpenAI.Chat;
using SampleApp;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
@@ -8,7 +8,7 @@
//
// Environment variables:
// AZURE_OPENAI_ENDPOINT
// AZURE_OPENAI_DEPLOYMENT_NAME (defaults to "gpt-4o-mini")
// AZURE_OPENAI_DEPLOYMENT_NAME (defaults to "gpt-5.4-mini")
//
// Run with: func start
// Then call: POST http://localhost:7071/api/agents/HostedAgent/run
@@ -23,7 +23,7 @@ using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
// Set up an AI agent following the standard Microsoft Agent Framework pattern.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
@@ -13,7 +13,7 @@ using Microsoft.Extensions.AI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var a2aAgentHost = Environment.GetEnvironmentVariable("A2A_AGENT_HOST") ?? throw new InvalidOperationException("A2A_AGENT_HOST is not set.");
// Initialize an A2ACardResolver to get an A2A agent card.
@@ -18,5 +18,5 @@ Set the following environment variables:
```powershell
$env:A2A_AGENT_HOST="https://your-a2a-agent-host" # Replace with your A2A agent host endpoint
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
```
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
@@ -13,7 +13,6 @@
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="System.Net.ServerSentEvents" />
</ItemGroup>
<ItemGroup>
@@ -18,8 +18,12 @@ AIAgent agent = agentCard.AsAIAgent();
AgentSession session = await agent.CreateSessionAsync();
// AllowBackgroundResponses must be true so the server returns immediately with a continuation token
// instead of blocking until the task is complete.
AgentRunOptions options = new() { AllowBackgroundResponses = true };
// Start the initial run with a long-running task.
AgentResponse response = await agent.RunAsync("Conduct a comprehensive analysis of quantum computing applications in cryptography, including recent breakthroughs, implementation challenges, and future roadmap. Please include diagrams and visual representations to illustrate complex concepts.", session);
AgentResponse response = await agent.RunAsync("Conduct a comprehensive analysis of quantum computing applications in cryptography, including recent breakthroughs, implementation challenges, and future roadmap. Please include diagrams and visual representations to illustrate complex concepts.", session, options: options);
// Poll until the response is complete.
while (response.ContinuationToken is { } token)
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="A2A" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to select the A2A protocol binding (HTTP+JSON vs JSON-RPC) when
// creating an AIAgent from an A2A agent card using A2AClientOptions.PreferredBindings.
using A2A;
using Microsoft.Agents.AI;
var a2aAgentHost = Environment.GetEnvironmentVariable("A2A_AGENT_HOST") ?? throw new InvalidOperationException("A2A_AGENT_HOST is not set.");
// Initialize an A2ACardResolver to get an A2A agent card.
A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost));
// Get the agent card
AgentCard agentCard = await agentCardResolver.GetAgentCardAsync();
// Use A2AClientOptions to explicitly select the HTTP+JSON protocol binding.
// This tells the A2A client factory to prefer the HTTP+JSON interface when the agent card
// advertises multiple supported interfaces.
A2AClientOptions options = new()
{
PreferredBindings = [ProtocolBindingNames.HttpJson]
};
// To prefer JSON-RPC instead, use:
// A2AClientOptions options = new()
// {
// PreferredBindings = [ProtocolBindingNames.JsonRpc]
// };
// Create an instance of the AIAgent for an existing A2A agent, using the specified protocol binding.
AIAgent agent = agentCard.AsAIAgent(options: options);
// Invoke the agent and output the text result.
AgentResponse response = await agent.RunAsync("Tell me a joke about a pirate.");
Console.WriteLine(response);
@@ -0,0 +1,27 @@
# A2A Agent Protocol Selection
This sample demonstrates how to select the A2A protocol binding when creating an `AIAgent` from an A2A agent card.
A2A agents can expose multiple interfaces with different protocol bindings (e.g., HTTP+JSON, JSON-RPC). By default, `AsAIAgent()` prefers HTTP+JSON with JSON-RPC as a fallback. This sample shows how to use `A2AClientOptions.PreferredBindings` to explicitly control which protocol binding is used.
The sample:
- Connects to an A2A agent server specified in the `A2A_AGENT_HOST` environment variable
- Configures `A2AClientOptions` to prefer the HTTP+JSON protocol binding
- Creates an `AIAgent` from the resolved agent card using the specified binding
- Sends a message to the agent and displays the response
## Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 10.0 SDK or later
- An A2A agent server running and accessible via HTTP
**Note**: These samples need to be run against a valid A2A server. If no A2A server is available, they can be run against the echo-agent that can be spun up locally by following the guidelines at: https://github.com/a2aproject/a2a-dotnet/blob/main/samples/AgentServer/README.md
Set the following environment variable:
```powershell
$env:A2A_AGENT_HOST="http://localhost:5000" # Replace with your A2A agent server host
```
@@ -6,18 +6,18 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>3afc9b74-af74-4d8e-ae96-fa1c511d11ac</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Agents.Persistent" />
<PackageReference Include="A2A" />
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="ModelContextProtocol" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to reconnect to an A2A agent's streaming response using continuation tokens,
// allowing recovery from stream interruptions without losing progress.
using A2A;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
var a2aAgentHost = Environment.GetEnvironmentVariable("A2A_AGENT_HOST") ?? throw new InvalidOperationException("A2A_AGENT_HOST is not set.");
// Initialize an A2ACardResolver to get an A2A agent card.
A2ACardResolver agentCardResolver = new(new Uri(a2aAgentHost));
// Get the agent card
AgentCard agentCard = await agentCardResolver.GetAgentCardAsync();
// Create an instance of the AIAgent for an existing A2A agent specified by the agent card.
AIAgent agent = agentCard.AsAIAgent();
AgentSession session = await agent.CreateSessionAsync();
ResponseContinuationToken? continuationToken = null;
await foreach (var update in agent.RunStreamingAsync("Conduct a comprehensive analysis of quantum computing applications in cryptography, including recent breakthroughs, implementation challenges, and future roadmap. Please include diagrams and visual representations to illustrate complex concepts.", session))
{
// Saving the continuation token to be able to reconnect to the same response stream later.
// Note: Continuation tokens are only returned for long-running tasks. If the underlying A2A agent
// returns a message instead of a task, the continuation token will not be initialized.
// A2A agents do not support stream resumption from a specific point in the stream,
// but only reconnection to obtain the same response stream from the beginning.
// So, A2A agents will return an initialized continuation token in the first update
// representing the beginning of the stream, and it will be null in all subsequent updates.
if (update.ContinuationToken is { } token)
{
continuationToken = token;
}
// Imitating stream interruption
break;
}
// Reconnect to the same response stream using the continuation token obtained from the previous run.
// As a first update, the agent will return an update representing the current state of the response at the moment of calling
// RunStreamingAsync with the same continuation token, followed by other updates until the end of the stream is reached.
if (continuationToken is not null)
{
await foreach (var update in agent.RunStreamingAsync(session, options: new() { ContinuationToken = continuationToken }))
{
if (!string.IsNullOrEmpty(update.Text))
{
Console.WriteLine(update.Text);
}
}
}
@@ -0,0 +1,29 @@
# A2A Agent Stream Reconnection
This sample demonstrates how to reconnect to an A2A agent's streaming response using continuation tokens, allowing recovery from stream interruptions without losing progress.
The sample:
- Connects to an A2A agent server specified in the `A2A_AGENT_HOST` environment variable
- Sends a request to the agent and begins streaming the response
- Captures a continuation token from the stream for later reconnection
- Simulates a stream interruption by breaking out of the streaming loop
- Reconnects to the same response stream using the captured continuation token
- Displays the response received after reconnection
This pattern is useful when network interruptions or other failures may disrupt an ongoing streaming response, and you need to recover and continue processing.
> **Note:** Continuation tokens are only available when the underlying A2A agent returns a task. If the agent returns a message instead, the continuation token will not be initialized and stream reconnection is not applicable.
# Prerequisites
Before you begin, ensure you have the following prerequisites:
- .NET 10.0 SDK or later
- An A2A agent server running and accessible via HTTP
Set the following environment variable:
```powershell
$env:A2A_AGENT_HOST="http://localhost:5000" # Replace with your A2A agent server host
```
@@ -3,7 +3,7 @@
These samples demonstrate how to work with Agent-to-Agent (A2A) specific features in the Agent Framework.
For other samples that demonstrate how to use AIAgent instances,
see the [Getting Started With Agents](../../02-agents/Agents/README.md) samples.
see the [Getting Started With Agents](../Agents/README.md) samples.
## Prerequisites
@@ -15,6 +15,8 @@ See the README.md for each sample for the prerequisites for that sample.
|---|---|
|[A2A Agent As Function Tools](./A2AAgent_AsFunctionTools/)|This sample demonstrates how to represent an A2A agent as a set of function tools, where each function tool corresponds to a skill of the A2A agent, and register these function tools with another AI agent so it can leverage the A2A agent's skills.|
|[A2A Agent Polling For Task Completion](./A2AAgent_PollingForTaskCompletion/)|This sample demonstrates how to poll for long-running task completion using continuation tokens with an A2A agent.|
|[A2A Agent Stream Reconnection](./A2AAgent_StreamReconnection/)|This sample demonstrates how to reconnect to an A2A agent's streaming response using continuation tokens, allowing recovery from stream interruptions.|
|[A2A Agent Protocol Selection](./A2AAgent_ProtocolSelection/)|This sample demonstrates how to select the A2A protocol binding (HTTP+JSON vs JSON-RPC) when creating an AIAgent from an A2A agent card using A2AClientOptions.|
## Running the samples from the console
+1 -1
View File
@@ -15,7 +15,7 @@ All samples require the following environment variables:
```bash
export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini"
```
For the client samples, you can optionally set:
@@ -59,18 +59,18 @@ while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringCompa
{
switch (content)
{
case FunctionApprovalRequestContent approvalRequest:
DisplayApprovalRequest(approvalRequest);
case ToolApprovalRequestContent approvalRequest when approvalRequest.ToolCall is FunctionCallContent fcc:
DisplayApprovalRequest(approvalRequest, fcc);
Console.Write($"\nApprove '{approvalRequest.FunctionCall.Name}'? (yes/no): ");
Console.Write($"\nApprove '{fcc.Name}'? (yes/no): ");
string? userInput = Console.ReadLine();
bool approved = userInput?.ToUpperInvariant() is "YES" or "Y";
FunctionApprovalResponseContent approvalResponse = approvalRequest.CreateResponse(approved);
ToolApprovalResponseContent approvalResponse = approvalRequest.CreateResponse(approved);
if (approvalRequest.AdditionalProperties != null)
{
approvalResponse.AdditionalProperties = new AdditionalPropertiesDictionary();
approvalResponse.AdditionalProperties = [];
foreach (var kvp in approvalRequest.AdditionalProperties)
{
approvalResponse.AdditionalProperties[kvp.Key] = kvp.Value;
@@ -128,19 +128,19 @@ while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringCompa
}
#pragma warning disable MEAI001
static void DisplayApprovalRequest(FunctionApprovalRequestContent approvalRequest)
static void DisplayApprovalRequest(ToolApprovalRequestContent approvalRequest, FunctionCallContent fcc)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine();
Console.WriteLine("============================================================");
Console.WriteLine("APPROVAL REQUIRED");
Console.WriteLine("============================================================");
Console.WriteLine($"Function: {approvalRequest.FunctionCall.Name}");
Console.WriteLine($"Function: {fcc.Name}");
if (approvalRequest.FunctionCall.Arguments != null)
if (fcc.Arguments != null)
{
Console.WriteLine("Arguments:");
foreach (var arg in approvalRequest.FunctionCall.Arguments)
foreach (var arg in fcc.Arguments)
{
Console.WriteLine($" {arg.Key} = {arg.Value}");
}
@@ -9,7 +9,7 @@ using ServerFunctionApproval;
/// <summary>
/// A delegating agent that handles server function approval requests and responses.
/// Transforms between FunctionApprovalRequestContent/FunctionApprovalResponseContent
/// Transforms between ToolApprovalRequestContent/ToolApprovalResponseContent
/// and the server's request_approval tool call pattern.
/// </summary>
internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
@@ -50,14 +50,14 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
}
#pragma warning disable MEAI001 // Type is for evaluation purposes only
private static FunctionResultContent ConvertApprovalResponseToToolResult(FunctionApprovalResponseContent approvalResponse, JsonSerializerOptions jsonOptions)
private static FunctionResultContent ConvertApprovalResponseToToolResult(ToolApprovalResponseContent approvalResponse, JsonSerializerOptions jsonOptions)
{
return new FunctionResultContent(
callId: approvalResponse.Id,
callId: approvalResponse.RequestId,
result: JsonSerializer.SerializeToElement(
new ApprovalResponse
{
ApprovalId = approvalResponse.Id,
ApprovalId = approvalResponse.RequestId,
Approved = approvalResponse.Approved
},
jsonOptions));
@@ -89,7 +89,7 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
{
List<ChatMessage>? result = null;
Dictionary<string, FunctionApprovalRequestContent> approvalRequests = [];
Dictionary<string, ToolApprovalRequestContent> approvalRequests = [];
for (var messageIndex = 0; messageIndex < messages.Count; messageIndex++)
{
var message = messages[messageIndex];
@@ -102,21 +102,21 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
var content = message.Contents[contentIndex];
// Handle pending approval requests (transform to tool call)
if (content is FunctionApprovalRequestContent approvalRequest &&
if (content is ToolApprovalRequestContent approvalRequest &&
approvalRequest.AdditionalProperties?.TryGetValue("original_function", out var originalFunction) == true &&
originalFunction is FunctionCallContent original)
{
approvalRequests[approvalRequest.Id] = approvalRequest;
approvalRequests[approvalRequest.RequestId] = approvalRequest;
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
transformedContents.Add(original);
}
// Handle pending approval responses (transform to tool result)
else if (content is FunctionApprovalResponseContent approvalResponse &&
approvalRequests.TryGetValue(approvalResponse.Id, out var correspondingRequest))
else if (content is ToolApprovalResponseContent approvalResponse &&
approvalRequests.TryGetValue(approvalResponse.RequestId, out var correspondingRequest))
{
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
transformedContents.Add(ConvertApprovalResponseToToolResult(approvalResponse, jsonSerializerOptions));
approvalRequests.Remove(approvalResponse.Id);
approvalRequests.Remove(approvalResponse.RequestId);
correspondingRequest.AdditionalProperties?.Remove("original_function");
}
// Skip historical approval content
@@ -131,9 +131,9 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
approvalCalls.Remove(functionResult.CallId);
}
else if (transformedContents != null)
else
{
transformedContents.Add(content);
transformedContents?.Add(content);
}
}
@@ -155,10 +155,10 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
result ??= CopyMessagesUpToIndex(messages, messageIndex);
result.Add(newMessage);
}
else if (result != null)
else
{
// We're already copying messages, so copy this unchanged message too
result.Add(message);
result?.Add(message);
}
// If result is null, we haven't made any changes yet, so keep processing
}
@@ -198,8 +198,8 @@ internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
var functionCallArgs = (Dictionary<string, object?>?)approvalRequest.FunctionArguments?
.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(Dictionary<string, object?>)));
var approvalRequestContent = new FunctionApprovalRequestContent(
id: approvalRequest.ApprovalId,
var approvalRequestContent = new ToolApprovalRequestContent(
requestId: approvalRequest.ApprovalId,
new FunctionCallContent(
callId: approvalRequest.ApprovalId,
name: approvalRequest.FunctionName,
@@ -9,7 +9,7 @@ using ServerFunctionApproval;
/// <summary>
/// A delegating agent that handles function approval requests on the server side.
/// Transforms between FunctionApprovalRequestContent/FunctionApprovalResponseContent
/// Transforms between ToolApprovalRequestContent/ToolApprovalResponseContent
/// and the request_approval tool call pattern for client communication.
/// </summary>
internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
@@ -50,44 +50,32 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
}
#pragma warning disable MEAI001 // Type is for evaluation purposes only
private static FunctionApprovalRequestContent ConvertToolCallToApprovalRequest(FunctionCallContent toolCall, JsonSerializerOptions jsonSerializerOptions)
private static ToolApprovalRequestContent ConvertToolCallToApprovalRequest(FunctionCallContent toolCall, JsonSerializerOptions jsonSerializerOptions)
{
if (toolCall.Name != "request_approval" || toolCall.Arguments == null)
{
throw new InvalidOperationException("Invalid request_approval tool call");
}
var request = toolCall.Arguments.TryGetValue("request", out var reqObj) &&
var request = (toolCall.Arguments.TryGetValue("request", out var reqObj) &&
reqObj is JsonElement argsElement &&
argsElement.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest))) is ApprovalRequest approvalRequest &&
approvalRequest != null ? approvalRequest : null;
if (request == null)
{
throw new InvalidOperationException("Failed to deserialize approval request from tool call");
}
return new FunctionApprovalRequestContent(
id: request.ApprovalId,
approvalRequest != null ? approvalRequest : null) ?? throw new InvalidOperationException("Failed to deserialize approval request from tool call");
return new ToolApprovalRequestContent(
requestId: request.ApprovalId,
new FunctionCallContent(
callId: request.ApprovalId,
name: request.FunctionName,
arguments: request.FunctionArguments));
}
private static FunctionApprovalResponseContent ConvertToolResultToApprovalResponse(FunctionResultContent result, FunctionApprovalRequestContent approval, JsonSerializerOptions jsonSerializerOptions)
private static ToolApprovalResponseContent ConvertToolResultToApprovalResponse(FunctionResultContent result, ToolApprovalRequestContent approval, JsonSerializerOptions jsonSerializerOptions)
{
var approvalResponse = result.Result is JsonElement je ?
var approvalResponse = (result.Result is JsonElement je ?
(ApprovalResponse?)je.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) :
result.Result is string str ?
(ApprovalResponse?)JsonSerializer.Deserialize(str, jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) :
result.Result as ApprovalResponse;
if (approvalResponse == null)
{
throw new InvalidOperationException("Failed to deserialize approval response from tool result");
}
result.Result as ApprovalResponse) ?? throw new InvalidOperationException("Failed to deserialize approval response from tool result");
return approval.CreateResponse(approvalResponse.Approved);
}
#pragma warning restore MEAI001
@@ -121,7 +109,7 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
// Track approval ID to original call ID mapping
_ = new Dictionary<string, string>();
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
Dictionary<string, FunctionApprovalRequestContent> trackedRequestApprovalToolCalls = new(); // Remote approvals
Dictionary<string, ToolApprovalRequestContent> trackedRequestApprovalToolCalls = []; // Remote approvals
for (int messageIndex = 0; messageIndex < messages.Count; messageIndex++)
{
var message = messages[messageIndex];
@@ -146,7 +134,7 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
});
}
else if (content is FunctionResultContent toolResult &&
trackedRequestApprovalToolCalls.TryGetValue(toolResult.CallId, out var approval) == true)
trackedRequestApprovalToolCalls.TryGetValue(toolResult.CallId, out var approval))
{
result ??= CopyMessagesUpToIndex(messages, messageIndex);
transformedContents ??= CopyContentsUpToIndex(message.Contents, j);
@@ -161,9 +149,9 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
AdditionalProperties = message.AdditionalProperties
});
}
else if (result != null)
else
{
result.Add(message);
result?.Add(message);
}
}
}
@@ -181,11 +169,10 @@ internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
{
var content = update.Contents[i];
#pragma warning disable MEAI001 // Type is for evaluation purposes only
if (content is FunctionApprovalRequestContent request)
if (content is ToolApprovalRequestContent request && request.ToolCall is FunctionCallContent functionCall)
{
updatedContents ??= [.. update.Contents];
var functionCall = request.FunctionCall;
var approvalId = request.Id;
var approvalId = request.RequestId;
var approvalData = new ApprovalRequest
{
@@ -72,10 +72,9 @@ internal sealed class StatefulAgent<TState> : DelegatingAIAgent
if (content is DataContent dataContent && dataContent.MediaType == "application/json")
{
// Deserialize the state
TState? newState = JsonSerializer.Deserialize(
if (JsonSerializer.Deserialize(
dataContent.Data.Span,
this._jsonSerializerOptions.GetTypeInfo(typeof(TState))) as TState;
if (newState != null)
this._jsonSerializerOptions.GetTypeInfo(typeof(TState))) is TState newState)
{
this.State = newState;
}
@@ -18,11 +18,12 @@ using OpenTelemetry.Trace;
#region Setup Telemetry
// Source name for this sample's custom ActivitySource and Meter; other instrumentation uses their own sources/categories.
const string SourceName = "OpenTelemetryAspire.ConsoleApp";
const string ServiceName = "AgentOpenTelemetry";
// Configure OpenTelemetry for Aspire dashboard
var otlpEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT") ?? "http://localhost:4318";
var otlpEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT") ?? "http://localhost:4317";
var applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING");
@@ -40,7 +41,6 @@ var resource = ResourceBuilder.CreateDefault()
var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
.AddSource(SourceName) // Our custom activity source
.AddSource("*Microsoft.Agents.AI") // Agent Framework telemetry
.AddHttpClientInstrumentation() // Capture HTTP calls to OpenAI
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint));
@@ -54,8 +54,7 @@ using var tracerProvider = tracerProviderBuilder.Build();
// Setup metrics with resource and instrument name filtering
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0"))
.AddMeter(SourceName) // Our custom meter
.AddMeter("*Microsoft.Agents.AI") // Agent Framework metrics
.AddMeter(SourceName) // Our custom meter source
.AddHttpClientInstrumentation() // HTTP client metrics
.AddRuntimeInstrumentation() // .NET runtime metrics
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint))
@@ -98,7 +97,7 @@ Console.WriteLine("""
""");
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT environment variable is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
// Log application startup
appLogger.LogInformation("OpenTelemetry Aspire Demo application started");
@@ -128,7 +127,7 @@ var agent = new ChatClientAgent(instrumentedChatClient,
instructions: "You are a helpful assistant that provides concise and informative responses.",
tools: [AIFunctionFactory.Create(GetWeatherAsync)])
.AsBuilder()
.UseOpenTelemetry(SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
.UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
.Build();
var session = await agent.CreateSessionAsync();
@@ -34,7 +34,7 @@ graph TD
Set the following environment variables:
```powershell
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
```
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource.

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