Compare commits

...
Author SHA1 Message Date
Tao Chen 20ae71251d remove temp 2026-04-27 20:54:01 -07:00
Tao Chen 48edeb3f67 Fix int tests 2026-04-27 20:51:15 -07:00
Tao Chen 8940b45d5b Address comments 2026-04-27 09:48:05 -07:00
Tao Chen 78c7d5fc84 Fix README 2026-04-24 17:27:02 -07:00
Tao Chen 9cf4c9169a Fix file content and add more tests 2026-04-24 17:18:21 -07:00
Tao Chen 2c6aa98b18 Add file data type support 2026-04-24 12:04:58 -07:00
Tao Chen 334ce4dfe0 Update foundry hosting samples 2026-04-24 11:48:17 -07:00
Evan MattsonandGitHub 0b69d7fd15 Python: Bump Python package versions for 1.2.0 release (#5468)
* Bump Python package versions for 1.2.0 release

Released tier bumps 1.1.1 -> 1.2.0 (core, openai, foundry, root) to
reflect additive public APIs landed since 1.1.0: functional workflow API
(#4238) and FunctionTool SKIP_PARSING sentinel (#5424). All beta packages
stamped 1.0.0b260424, alpha packages 1.0.0a260424. All 26 non-core
agent-framework-core floors raised to >=1.2.0,<2. CHANGELOG consolidates
the never-tagged 1.1.1 entries with the post-merge additions into [1.2.0].

* Update CHANGELOG footer links for 1.2.0

Advance [Unreleased] comparison base from python-1.1.0 to python-1.2.0
and add a [1.2.0] reference link comparing python-1.1.0...python-1.2.0
so the heading links resolve correctly.

* Fix CHANGELOG: restore [1.1.1] section and add proper [1.2.0]

Previous commit incorrectly renamed the [1.1.1] header to [1.2.0], which
wiped the historical 1.1.1 entries and wrongly attributed them to 1.2.0.
This restores [1.1.1] to its origin/main content and adds a new [1.2.0]
section above containing only the commits in python-1.1.1..HEAD:

- #4238 functional workflow API
- #5142 GitHub Copilot OpenTelemetry
- #2403 A2A bridge support
- #5070 oauth_consent_request events in Foundry clients
- #5447 FoundryAgent hosted agent sessions
- #5459 hosting server dependency upgrade + types
- #5389 AG-UI reasoning/multimodal parsing fix
- #5440 stop [TOOLBOXES] warning spam
- #5455 user agent prefix fix

Also corrects the [1.2.0] compare base to python-1.1.1 (not 1.1.0) and
adds the missing [1.1.1] reference link.
2026-04-24 19:54:59 +09:00
7b70f80036 Python: Surface oauth_consent_request events from Responses API in Foundry clients (#5070)
* Fix Foundry clients not surfacing oauth_consent_request events (#5054)

Override _parse_chunk_from_openai in both RawFoundryChatClient and
RawFoundryAgentChatClient to intercept response.output_item.added
events with item.type == 'oauth_consent_request'. The consent link
is validated (HTTPS required) and converted to
Content.from_oauth_consent_request, which the AG-UI layer already
knows how to emit as a CUSTOM event.

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

* Address PR review feedback for #5054 OAuth consent parsing

- Extract shared helper (try_parse_oauth_consent_event) to avoid
  duplicated logic between RawFoundryChatClient and
  RawFoundryAgentChatClient
- Use urllib.parse.urlparse() for HTTPS validation instead of
  case-sensitive startswith check
- Sanitize log messages to avoid leaking consent_link tokens;
  log only item id
- Add model=self.model to ChatResponseUpdate to match parent behavior
- Add assertions on role, raw_representation, and model in happy-path
  tests
- Add test for empty-string consent_link
- Add test verifying non-oauth events delegate to super()

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

* Handle response.oauth_consent_requested top-level event (#5054)

Add support for the top-level response.oauth_consent_requested stream
event in addition to the response.output_item.added variant. The
service may emit either form; handle both so the consent link is
reliably surfaced.

Extract _validate_consent_link helper within _oauth_helpers.py to
reduce nesting.

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

* Address review feedback for #5054: Python: [Bug]: `FoundryAgent` (Responses API) Does Not Surface `oauth_consent_request` as a CUSTOM AG-UI Event

* Address review feedback: defensive getattr and dedicated helper tests (#5054)

- Use getattr(event, 'type', None) in try_parse_oauth_consent_event
  for defensive access against malformed events without a type attribute
- Add test_oauth_helpers.py with unit tests for _validate_consent_link
  and try_parse_oauth_consent_event covering edge cases:
  - HTTPS URL with empty netloc (https:///path)
  - Warning log messages for rejected consent links
  - Event objects missing 'type' attribute

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

* Address review feedback for #5054: Python: [Bug]: `FoundryAgent` (Responses API) Does Not Surface `oauth_consent_request` as a CUSTOM AG-UI Event

* Fix mypy: match _parse_chunk_from_openai signature with superclass

Add seen_reasoning_delta_item_ids parameter to _parse_chunk_from_openai
overrides in both RawFoundryChatClient and RawFoundryAgentChatClient to
match the updated superclass signature on main. Update super() calls and
test assertions accordingly.

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>
Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
2026-04-24 09:59:14 +00:00
da32e8cf80 Python: (core): Add functional workflow API (#4238)
* Add functional workflow api

* cleanup

* More cleanup

* address copilot feedback

* Address PR feedbacK

* updates

* PR feedback

* Address review comments on functional workflow samples

- Swap 05/06 get-started samples: agent workflow first (motivates
  why workflows exist), simple text workflow second
- Rename text_pipeline → text_workflow, poem_pipeline → poem_workflow
- Add @step to agent workflow sample (05) to demonstrate caching
- Switch agent samples to AzureOpenAIResponsesClient with Foundry
- Remove .as_agent() from agent_integration.py to focus on the key
  difference between inline agent calls vs @step-cached calls
- Add commented-out Agent.run example in hitl_review.py
- Add clarifying comment in _functional.py that event streaming is
  buffered (not true per-token streaming)
- Add naive_group_chat.py functional sample: round-robin group chat
  as a plain Python loop
- Update READMEs to reflect new file names and group chat sample

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

* Fix pyright type errors

* Address PR review comments on functional workflow API

1. Allow request_info inside @step: Auto-inject RunContext into step
   functions that declare a RunContext parameter (by type or name 'ctx'),
   and expose get_run_context() for programmatic access.

2. Handle None responses: Log a warning when a response value is None,
   and document the behavior in request_info docstring.

3. Add executor_bypassed event type: Replace executor_invoked +
   executor_completed with a single executor_bypassed event when a step
   replays from cache, making cached vs live execution explicit.

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

* Add regression tests for PR review comments on functional workflow API

The three review comments (request_info in @step, None response handling,
executor_bypassed event type) were already addressed in 7da7db4e. This
commit adds cross-cutting regression tests that exercise the interactions
between these features:

- HITL in step with caching: preceding step bypassed on resume
- Full checkpoint lifecycle with HITL step (interrupt -> resume -> restore)
- None response inside step-level request_info logs warning
- WorkflowInterrupted from step does not emit executor_failed

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

* Address PR #4238 review comments on functional workflow API

Comment 1 (request_info in @step): Already supported. Added comment in
StepWrapper.__call__ explaining why WorkflowInterrupted (BaseException)
safely bypasses the except Exception handler.

Comment 2 (None response): Added docstring to _get_response clarifying
the (found, value) return tuple semantics and None handling.

Comment 3 (bypass event type): executor_bypassed is already a dedicated
event type in WorkflowEventType. Updated comment at the bypass site to
make the deliberate event type choice explicit.

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

* Add experimental API warnings to functional workflow module

Mark all public classes and decorators (workflow, step, RunContext,
FunctionalWorkflow, StepWrapper, FunctionalWorkflowAgent) as
experimental and subject to change or removal.

* Address PR #4238 review comments from @eavanvalkenburg

- RunContext docstring leads with purpose (opt-in handle for HITL,
  custom events, state) so readers importing it from the public surface
  understand its role before the mechanics (#2993513452).
- Rename `06_first_functional_workflow.py` to
  `06_functional_workflow_basics.py`; the previous filename was
  confusing since it followed `05_functional_workflow_with_agents.py`
  (#2993531979).
- Simplify `05_functional_workflow_with_agents.py` to call agents
  directly without a @step wrapper; the step-vs-no-step contrast lives
  in `03-workflows/functional/agent_integration.py`, keeping the
  get-started sample minimal (#2993525532).
- Switch functional samples to `FoundryChatClient` for consistency with
  the rest of 01-get-started and 03-workflows (follow-up on #2876988570).
- Use walrus in `hitl_review.py` final-state assertion (#2993572182).
- Add expected-output block to `basic_streaming_pipeline.py` (#2993557609).
- Clarify in `parallel_pipeline.py` that `@step` composes with
  `asyncio.gather` (#2993597282).
- `naive_group_chat.py` threads `list[Message]` between turns instead
  of stringifying the transcript, preserving role/authorship (#2993583231).

Drive-by: pre-commit hook sorts an unrelated import block in
`samples/04-hosting/foundry-hosted-agents/responses/02_local_tools/main.py`.

* Fix 10 functional-workflow API bugs from /ultrareview pass

- bug_001: `ctx.request_info()` without an explicit `request_id` now derives
  a deterministic `auto::<index>` id from the call-counter, so HITL resume
  works correctly on the documented default path.  A uuid was regenerated on
  every replay, making resume impossible.

- bug_002: `StepWrapper.__call__` no longer deepcopies arguments on the
  cache-hit replay branch.  The copy is only performed on the live-execution
  path (for the event log) and falls back to the original mapping if deepcopy
  fails, so steps whose args aren't deepcopyable (locks, sockets, sessions)
  can still resume from checkpoint.

- bug_007: `_set_responses` now prunes each resolved `request_id` from
  `_pending_requests`, and the cache-hit branch in `request_info` does the
  same.  Previously, answered requests were re-serialized into every
  subsequent checkpoint and the final checkpoint falsely claimed pending
  requests even after the workflow completed.

- bug_008: `_compute_signature_hash` now mixes the function's `co_code` and
  `co_names` into the checkpoint signature, so changes to the workflow body
  invalidate older checkpoints even when steps are accessed via module /
  class attributes (which `_discover_step_names` can't see statically).
  `RunContext._record_observed_step` records observed step names for
  diagnostics.

- bug_010: `FunctionalWorkflow.run()` docstring corrected — says "at least
  one of message/responses/checkpoint_id" and explicitly notes `responses`
  may be combined with `checkpoint_id` (the validator already allowed this).

- bug_013: `FunctionalWorkflowAgent` now surfaces `request_info` events as
  `FunctionApprovalRequestContent` items (mirroring graph `WorkflowAgent`),
  threads `responses=` and `checkpoint_id=` through to the underlying
  workflow, and exposes `pending_requests`.  Previously `.as_agent()`
  returned empty `AgentResponse` for HITL workflows — effectively unusable.

- bug_014: `FunctionalWorkflow` now clears `_last_message`,
  `_last_step_cache`, and `_last_pending_request_ids` on clean completion.
  `run()` validates that `responses=` keys intersect the currently-pending
  request set (or raises with a clear error) instead of silently replaying
  against stale singleton state from a prior run.

- bug_015: `FunctionalWorkflow.as_agent` signature now matches graph
  `Workflow.as_agent`: accepts `name`, `description`, `context_providers`,
  and `**kwargs`.  `FunctionalWorkflowAgent` stores the overrides.

- bug_017: `RunContext.set_state` raises `ValueError` for underscore-
  prefixed keys (the framework's `_step_cache` / `_original_message` keys
  would silently clobber user state on checkpoint save and user
  underscore-prefixed state was dropped on restore).  Docstring documents
  the reserved prefix.

- merged_bug_003: Workflow function arity is validated at decoration time.
  Multiple non-ctx parameters raise `ValueError` immediately (previously
  every arg past the first was silently dropped at call time).  Passing a
  non-None `message` to a ctx-only workflow raises `ValueError` instead of
  silently discarding the message.

Test coverage: +18 regression tests covering every fix.  Full workflow
suite now 766 passed, 1 skipped, 2 xfailed; full core suite 2338 passed.

* Deslop functional.py fix commit

- Remove dead instrumentation added in the prior commit that was never
  consumed: `RunContext._observed_step_names`,
  `RunContext._record_observed_step`, `FunctionalWorkflow._runtime_step_names`,
  and `FunctionalWorkflowAgent._extra_kwargs`.  The signature hash relies on
  `co_code` alone, which covers the attribute-access case without the
  collection-scaffolding.
- Trim over-explanatory comments that restated what the code does or what
  it no longer does.  Keep only the comments that answer "why" for the
  non-obvious bits (deterministic id contract, defensive deepcopy, stale
  replay guard).
- Compress the `_compute_signature_hash` and FunctionalWorkflow `__init__`
  block docstrings without losing the user-facing reasoning.

Net -49 lines.  Regression lock preserved (766 passed, 1 skipped, 2 xfailed).

* Fix functional workflow review feedback

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
2026-04-24 09:41:20 +00:00
62e02da698 Python: update FoundryAgent for hosted agent sessions (#5447)
* fixes to FoundryAgent to connect to new hosted agents

Co-authored-by: Copilot <copilot@github.com>

* fix mypy

Co-authored-by: Copilot <copilot@github.com>

* Python: remove Foundry service session helpers

Remove the public hosted-agent service session CRUD helpers from FoundryAgent and drop the related feature-stage inventory entry.

Update the hosted-agent sample to create and delete service sessions directly through the preview AIProjectClient APIs, and tighten a few test harnesses surfaced by full workspace validation.

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

* fix from merge

* fix hosted env detection

Co-authored-by: Copilot <copilot@github.com>

* reverted sample update

* fix tests and code

Co-authored-by: Copilot <copilot@github.com>

* remove aenter

* skipping some tests

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-24 09:25:03 +00:00
63c0a51797 Python: Add OpenTelemetry integration for GitHubCopilotAgent (#5142)
* Python: Add OpenTelemetry integration for GitHubCopilotAgent

- Split GitHubCopilotAgent into RawGitHubCopilotAgent (core, no OTel) and
  GitHubCopilotAgent(AgentTelemetryLayer, RawGitHubCopilotAgent) with tracing
- Add default_options property to expose model for span attributes
- Export RawGitHubCopilotAgent from all public namespaces
- Add github_copilot_with_observability.py sample and update README

* Python: Fix OTEL_SERVICE_NAME default in GitHub Copilot README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Python: Add unit tests for RawGitHubCopilotAgent.default_options property

* Python: Address review feedback on GitHubCopilotAgent OTel integration

- Add middleware param to GitHubCopilotAgent.run() overloads so per-call
  middleware is explicitly forwarded through AgentTelemetryLayer
- Remove github_copilot_with_observability.py sample per feedback; replace
  with inline snippet + link to observability samples in README

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

- Add middleware param to RawGitHubCopilotAgent.run() overloads for interface
  compatibility with AgentTelemetryLayer
- Fix import in README observability snippet to use agent_framework.github

* Python: Add AgentMiddlewareLayer to GitHubCopilotAgent MRO

Follow FoundryAgent pattern: AgentMiddlewareLayer runs outside the telemetry
span so middleware execution time is not captured in traces. Overloads removed
as AgentMiddlewareLayer.run() handles dispatch via MRO.

* Python: Add explicit __init__ to GitHubCopilotAgent for auto-complete and docstrings

* Python: Address review feedback on middleware warning and test assertions

- Add assert "timeout" not in opts to test_default_options_includes_model_for_telemetry
  to document the intentional asymmetry where timeout is extracted into _settings
  and not returned in default_options.
- Replace silent del middleware with a logged warning when per-run middleware is
  passed to RawGitHubCopilotAgent, making it clear that the GitHub Copilot SDK
  handles tool execution internally and chat/function middleware cannot be injected.

* Python: Use Self for __aenter__ return type in RawGitHubCopilotAgent

Address review feedback: use typing.Self (3.11+) / typing_extensions.Self
(3.10) for __aenter__ so subclasses like GitHubCopilotAgent get the correct
return type from async context manager usage.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 08:44:44 +00:00
b00465d7be Python: feat: Add Agent Framework to A2A bridge support (#2403)
* feat: Add Agent Framework to A2A bridge support

- Implement A2A event adapter for converting agent messages to A2A protocol
- Add A2A execution context for managing agent execution state
- Implement A2A executor for running agents in A2A environment
- Add comprehensive unit tests for event adapter, execution context, and executor
- Update agent framework core A2A module exports and type stubs
- Integrate thread management utilities for async execution
- Add getting started sample for A2A agent framework integration
- Update dependencies in uv.lock

This integration enables agent framework agents to communicate and execute within the A2A (Agent to Agent) infrastructure.

* fix: Update references from agent_thread_storage to _agent_thread_storage in A2A executor tests

* Refactor A2A agent framework and improve code structure

- Reordered imports in various files for consistency and clarity.
- Updated `__all__` definitions to maintain a consistent order across modules.
- Simplified method signatures by removing unnecessary line breaks.
- Enhanced readability by adjusting formatting in several sections.
- Removed redundant comments and example scenarios in the execution context.
- Improved handling of agent messages in the event adapter.
- Added type hints for better clarity and type checking.
- Cleaned up test cases for better organization and readability.

* fix: Lint fix new line added

* test: Add unit tests for AgentThreadStorage and InMemoryAgentThreadStorage

* refactor: Update type hints to use new syntax for Union and List

* fix: Validate RequestContext for context_id and message before execution

* Refactor tests and remove A2aExecutionContext references

- Deleted the test file for A2aExecutionContext as it is no longer needed.
- Updated A2aExecutor tests to remove dependencies on A2aExecutionContext and adjusted method calls accordingly.
- Modified event adapter tests to use ChatMessage instead of AgentRunResponseUpdate.
- Removed A2aExecutionContext from imports in agent_framework.a2a module and updated type hints accordingly.

* Refactor A2AExecutor tests and remove event adapter

- Updated test cases to use A2AExecutor instead of A2aExecutor for consistency.
- Removed mock_event_adapter fixture and related tests as A2aEventAdapter is deprecated.
- Consolidated event handling tests into TestA2AExecutorEventAdapter.
- Adjusted imports in various files to reflect the removal of deprecated components.
- Ensured all references to A2aExecutor are updated to A2AExecutor across the codebase.

* refactor: Remove AgentThreadStorage and InMemoryAgentThreadStorage classes from threads and tests

* feat: A2AExecutor to have its own override able save and get threads methods for persistent storage.

* fix: linter bugs

* removed unnecessary changes form core package

* new line added

* Refactor A2AExecutor tests and update imports

- Consolidated mock agent fixtures in test_a2a_executor.py to simplify agent mocking.
- Removed redundant tests related to thread storage and agent types, focusing on A2AExecutor's core functionality.
- Updated test assertions to reflect changes in message handling with new Message and Content classes.
- Enhanced integration tests to ensure compatibility with the new agent framework structure.
- Added A2AExecutor to the module exports in __init__.py and __init__.pyi for better accessibility.

* Update A2A documentation: enhance usage examples for A2AAgent and A2AExecutor

* Updated uv lock

* Fix metadata assertion in TestA2AExecutorHandleEvents and reorder load_dotenv call in agent_framework_to_a2a.py

* Update agent card configuration: add default input and output modes, and fix agent creation method

* Fix assertion for metadata in TestA2AExecutorHandleEvents

* Fix formatting issues in TestA2AExecutorExecute and TestA2AExecutorIntegration

* Enhance A2AExecutor documentation with examples and clarify agent execution process

* Revert uv lock to main

* Refactor A2AExecutor: Improve formatting and streamline constructor parameters

* Apply suggestions from code review

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

* Refactor A2AExecutor to use SupportsAgentRun and enhance logging; update agent framework sample for flight and hotel booking capabilities

* Enhance A2AExecutor with streaming support and custom run arguments; update tests for initialization and execution scenarios

* Enhance A2AExecutor event handling with streamed artifact tracking; update tests for new behavior

* Refactor A2AExecutor to enforce type hints for stream and run_kwargs attributes

* Refactor A2AExecutor and tests: replace AsyncMock with MagicMock for response stream handling; clean up imports in agent_framework_to_a2a.py

* refactor: streamline imports and improve code readability across multiple files

* feat: enhance A2AExecutor cancel method with context validation and fixed review comments

* feat: implement get_uri_data utility function for extracting base64 data from data URIs and update references

* fix: update import path for get_uri_data utility function in A2AExecutor and A2AAgent

* fix: correct error message handling in A2AExecutor and update test assertions

---------

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-04-24 08:35:40 +00:00
Tao ChenandGitHub 4adfd244ac Python: Upgrade hosting server dependency and add more type support (#5459)
* Upgrade hosting server dependency and add more type support

* Comments
2026-04-24 07:27:17 +00:00
932ceddf95 Python: Fix AG-UI reasoning role and multimodal media parsing to follow specification (#5389)
* Fix AG-UI reasoning role and multimodal media value field parsing

Fix two spec compliance issues in the AG-UI integration:

1. ReasoningMessageStartEvent now uses role='reasoning' instead of
   role='assistant', matching the AG-UI specification for reasoning
   messages.

2. _parse_multimodal_media_part now reads the 'value' field from source
   dicts (with fallback to 'data' for backward compatibility), matching
   the current AG-UI InputContentSource specification.

Bump ag-ui-protocol dependency from ==0.1.13 to >=0.1.16,<0.2 to pick
up the SDK fix that accepts role='reasoning' in ReasoningMessageStartEvent.

Fix pre-existing pyright reportMissingImports errors for orjson in sample
files, and fix import ordering in foundry-hosted-agents sample.

Fixes #5340

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

* Python: Fix AG-UI reasoning role and multimodal media parsing to follow specification

Fixes #5340

* Remove unintended .maf-runtime-ready marker file

Address PR review feedback: the .maf-runtime-ready file is not referenced anywhere in the repo and was left over from automation.

Fixes #5340

* Python: Fix duplicate AG-UI multimodal 'value' parsing in snapshot path

The snapshot normalization path used a second copy of the multimodal source
parsing logic that still read the deprecated 'data' field. When clients sent
base64 media with source={"type": "base64", "value": ...}, the snapshot event
emitted by the server dropped the payload, causing AG-UI-compatible clients
to crash on ingest.

Extract the shared source-field extraction into _extract_multimodal_source_fields
so both _parse_multimodal_media_part and the snapshot _legacy_binary_part stay
in sync with the AG-UI spec. Add snapshot-path regression tests covering
value-only, value-preferred-over-data, and the legacy data-field fallback.

Addresses review feedback on #5389 from @Rickyneer.

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-24 04:12:34 +00:00
Tao ChenandGitHub 0989e68d1c Python: Fix user agent prefix (#5455)
* Fix hosting user agent missing

* Fix other providers

* Add more tests

* comments

* Fix tests
2026-04-23 23:40:38 +00:00
Evan MattsonandGitHub b084d0461d Python: (foundry): stop emitting [TOOLBOXES] warning for every FoundryChatClient call (#5440)
* Python: Foundry: make response tool sanitizer internal, drop TOOLBOXES warning

sanitize_foundry_response_tool runs on every tool passed to the Foundry
Responses API, so its @experimental(TOOLBOXES) decorator was emitting a
[TOOLBOXES] ExperimentalWarning for any FoundryChatClient call, even when
no toolbox was involved. The function isn't in __all__ and has no external
callers. Rename to _sanitize_foundry_response_tool and drop the decorator;
the actual toolbox-facing public helpers remain gated.

* Python: Foundry: silence pyright on intentional cross-module private import
2026-04-23 22:19:37 +00:00
5fe8941ff9 .NET: dotnet: Add server-side Foundry Toolbox support and fix SDK beta.4 br… (#5450)
* dotnet: Add server-side Foundry Toolbox support and fix SDK beta.4 breaking changes

Add FoundryToolbox and AIProjectClient extensions to Microsoft.Agents.AI.Foundry.Hosting
for server-side toolbox tool integration matching Python's FoundryChatClient.get_toolbox()
pattern. Tools are fetched from the Foundry project SDK and passed as server-side tools
in the Responses API request.

New files:
- FoundryToolbox.cs: Core implementation using AgentAdministrationClient SDK
- AIProjectClientToolboxExtensions.cs: Extension methods on AIProjectClient
- Agent_Step25_ToolboxServerSideTools sample with create helper and combine flow
- 19 unit tests covering param validation, conversion, sanitization, and extensions

SDK breaking changes (Azure.AI.AgentServer.Responses beta.3 -> beta.4):
- FunctionToolCallOutputResource renamed to OutputItemFunctionToolCallOutput
- AzureAIAgentServerResponsesModelFactory made internal, replaced with direct constructors
- ResponseUsage constructor now requires non-null token details parameters

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

* fix: reuse endpoint variable in CreateSampleToolboxAsync

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

* fix: pass endpoint through static local functions to avoid capture

Static local functions cannot capture top-level variables. Thread the
endpoint parameter through Main, CombineToolboxes, and CreateSampleToolboxAsync.

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

* refactor: remove unused projectClient param from CreateSampleToolboxAsync

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

* Update dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/README.md

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

* Update dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/Program.cs

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

* Update dotnet/samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/Program.cs

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

* Removing GetToolbocVersion.

* Removing tests for GetToolboxVersion

* fix: map cached/reasoning token counts in ConvertUsage instead of hardcoding zeros

Extract InputTokenDetails.CachedTokenCount and OutputTokenDetails.ReasoningTokenCount
from UsageDetails.AdditionalCounts, matching the pattern in AgentResponseExtensions.
Also accumulate detail counts when merging with existing usage.

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: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
2026-04-23 18:52:03 +00:00
0dbcc9fe9d .NET: Add streaming support to A2A agent handler (#5427)
* 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 streaming support to A2A agent handler

Add HandleNewMessageStreamingAsync to A2AAgentHandler that routes
StreamingResponse requests through RunStreamingAsync, enqueuing an A2A
Message for each AgentResponseUpdate.

Add MessageConverter.ToParts(AgentResponseUpdate) extension to convert
streaming update contents to A2A Parts with unsupported-content filtering.

Add CreateMessageFromUpdate to map AgentResponseUpdate to A2A Message.

Add 16 new tests covering the streaming path and converter.

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

* Add streaming edge-case tests for A2AAgentHandler

Add two tests covering gaps in the streaming path:

- ExecuteAsync_Streaming_WhenNoUpdates_EnqueuesNoMessagesAndSavesSessionAsync:
  Verifies that when RunStreamingAsync yields an empty async enumerable,
  no messages are enqueued and only SaveSessionAsync runs.

- ExecuteAsync_Streaming_CancellationTokenIsPropagatedToRunStreamingAsyncAsync:
  Verifies that the CancellationToken from ExecuteAsync is propagated
  through to the inner agent's RunCoreStreamingAsync call.

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

* address copilot comments

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-23 18:50:57 +00:00
westeyandGitHub 4d3e4f865f Update versions for release (#5449) 2026-04-23 18:26:55 +00:00
69adf6d97e .NET: Fix off-thread RunStatus race where GetStatusAsync can return Running after ResumeAsync halts (#5412)
* Fix off-thread RunStatus race where GetStatusAsync can return Running after ResumeAsync halts

* Apply suggestion from @Copilot

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

* Simplify test comment.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-23 15:27:28 +00:00
westeyandGitHub 6851a9cdc8 .NET: Add dynamic tool expansion sample (#5425)
* Add dynamic tool expansion sample

* Address PR comments

* Remove tool names from tool call response to avoid confusing LLM
2026-04-23 15:10:57 +00:00
westeyandGitHub dfca81ff21 .NET: Update Aspire package to be preview (#5444)
* Update Aspire package to be preview

* Also update readme file

* Include README.md in pack
2026-04-23 15:10:49 +00:00
Evan MattsonandGitHub fbbc2ebe86 Propagate integration-test model credentials to issue-triage repro (#5443)
Scopes the triage job to the integration GitHub Environment, adds
the azure/login OIDC step, and exposes the same OpenAI / Azure
OpenAI / Foundry / Anthropic env vars the integration test
workflow uses. This lets the triage agent write repro code that
constructs model clients from the environment without any secrets
entering the agent prompt or generated-code literals.

Azure OpenAI and Foundry continue to authenticate via AAD
(DefaultAzureCredential), so there is no API key to leak for
those providers.
2026-04-23 21:01:24 +09:00
c9e6033048 Automated issue triage workflow (#5419)
* Automated issue triage workflow

* Bump dependencies

* Fix issue-triage workflow: security, reliability, and testability

Address six review comments on the issue-triage workflow:

1. Change trigger from issues:opened to issues:labeled so the
   secret-backed triage flow is only triggered by a maintainer-
   controlled signal.

2. Include inputs.issue_number in the concurrency group so
   workflow_dispatch runs for the same issue are properly
   de-duplicated.

3. Improve team membership error handling to fail closed: verify
   the team exists before checking membership, and only treat a
   404 as 'not a member' (all other errors fail the job).

4. Use optional chaining (issue.user?.login) for the API-fetched
   issue to handle deleted GitHub accounts without crashing.

5. Extract the inline github-script into a testable module at
   .github/scripts/check_team_membership.js with 10 tests in
   .github/tests/test_check_team_membership.js covering all
   code paths (payload/API author resolution, deleted accounts,
   team lookup failure, 404 vs non-404 membership errors).

6. Make the spam gate actually stop the job by exiting non-zero
   instead of just logging, so future steps cannot accidentally
   run for spam issues.

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

* Make issue-triage workflow manually triggered only for initial testing

Remove the 'issues' event trigger, keeping only 'workflow_dispatch' so the
workflow can be tested manually before enabling automatic triggers.

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-23 20:22:04 +09:00
9ca55dcc0c Python: (chore): update changelog (#5438)
* update changelog

* Update python/CHANGELOG.md

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

* Update python/CHANGELOG.md

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-23 17:50:29 +09:00
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
441 changed files with 31021 additions and 5968 deletions
+61
View File
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All rights reserved.
/**
* Resolve the issue author and check their team membership.
*
* @param {object} opts
* @param {object} opts.github - Octokit REST client from actions/github-script
* @param {object} opts.context - GitHub Actions context
* @param {object} opts.core - GitHub Actions core toolkit
* @param {string} opts.teamSlug - Team slug to check membership against
* @param {string|number} opts.issueNumber - Issue number to resolve author for
* @returns {Promise<{author: string|null, isTeamMember: boolean}>}
*/
async function checkTeamMembership({ github, context, core, teamSlug, issueNumber }) {
let author = context.payload.issue?.user?.login;
if (!author) {
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: Number(issueNumber),
});
author = issue.user?.login;
}
if (!author) {
core.setFailed('Could not determine issue author (user may be deleted).');
return { author: null, isTeamMember: false };
}
try {
await github.rest.teams.getByName({
org: context.repo.owner,
team_slug: teamSlug,
});
} catch (error) {
core.setFailed(`Team lookup failed for ${teamSlug}: ${error.message}`);
throw error;
}
let isTeamMember = false;
try {
const teamMembership = await github.rest.teams.getMembershipForUserInOrg({
org: context.repo.owner,
team_slug: teamSlug,
username: author,
});
isTeamMember = teamMembership.data.state === 'active';
} catch (error) {
if (error.status === 404) {
core.info(`Author ${author} is not a member of team ${teamSlug}.`);
isTeamMember = false;
} else {
core.setFailed(`Team membership lookup failed for ${author}: ${error.message}`);
throw error;
}
}
return { author, isTeamMember };
}
module.exports = checkTeamMembership;
+178
View File
@@ -0,0 +1,178 @@
// Copyright (c) Microsoft. All rights reserved.
/**
* Tests for check_team_membership.js.
*
* Run with: node --test .github/tests/test_check_team_membership.js
*/
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const checkTeamMembership = require('../scripts/check_team_membership.js');
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function createMocks({ payloadIssue = undefined, apiUser = 'api-user', teamState = 'active' } = {}) {
const core = {
_infoMessages: [],
_failedMessages: [],
info(msg) { this._infoMessages.push(msg); },
setFailed(msg) { this._failedMessages.push(msg); },
};
const context = {
payload: { issue: payloadIssue },
repo: { owner: 'test-org', repo: 'test-repo' },
};
const github = {
rest: {
issues: {
get: async () => ({
data: { user: apiUser ? { login: apiUser } : null },
}),
},
teams: {
getByName: async () => ({}),
getMembershipForUserInOrg: async () => ({
data: { state: teamState },
}),
},
},
};
return { core, context, github };
}
const BASE_OPTS = { teamSlug: 'my-team', issueNumber: '123' };
// ---------------------------------------------------------------------------
// Author resolution
// ---------------------------------------------------------------------------
describe('author resolution', () => {
it('resolves author from event payload', async () => {
const { github, context, core } = createMocks({
payloadIssue: { user: { login: 'payload-user' } },
});
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
assert.equal(result.author, 'payload-user');
});
it('resolves author via API when payload issue is absent', async () => {
const { github, context, core } = createMocks({ apiUser: 'api-user' });
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
assert.equal(result.author, 'api-user');
});
it('resolves author via API when payload issue user is null (deleted account)', async () => {
const { github, context, core } = createMocks({
payloadIssue: { user: null },
apiUser: 'fetched-user',
});
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
assert.equal(result.author, 'fetched-user');
});
it('handles deleted account when API also returns null user', async () => {
const { github, context, core } = createMocks({ apiUser: null });
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
assert.equal(result.author, null);
assert.equal(result.isTeamMember, false);
assert.ok(core._failedMessages.some(m => m.includes('deleted')));
});
});
// ---------------------------------------------------------------------------
// Team lookup
// ---------------------------------------------------------------------------
describe('team lookup', () => {
it('fails the job when team lookup errors', async () => {
const { github, context, core } = createMocks({
payloadIssue: { user: { login: 'user1' } },
});
const error = new Error('Bad credentials');
github.rest.teams.getByName = async () => { throw error; };
await assert.rejects(
() => checkTeamMembership({ github, context, core, ...BASE_OPTS }),
(err) => err === error,
);
assert.ok(core._failedMessages.some(m => m.includes('Team lookup failed')));
});
});
// ---------------------------------------------------------------------------
// Team membership
// ---------------------------------------------------------------------------
describe('team membership', () => {
it('returns true for active team member', async () => {
const { github, context, core } = createMocks({
payloadIssue: { user: { login: 'member' } },
teamState: 'active',
});
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
assert.equal(result.isTeamMember, true);
});
it('returns false for pending team member', async () => {
const { github, context, core } = createMocks({
payloadIssue: { user: { login: 'pending-user' } },
teamState: 'pending',
});
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
assert.equal(result.isTeamMember, false);
});
it('treats 404 membership response as non-member without failing', async () => {
const { github, context, core } = createMocks({
payloadIssue: { user: { login: 'outsider' } },
});
const notFoundError = new Error('Not Found');
notFoundError.status = 404;
github.rest.teams.getMembershipForUserInOrg = async () => { throw notFoundError; };
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
assert.equal(result.isTeamMember, false);
assert.equal(core._failedMessages.length, 0);
assert.ok(core._infoMessages.some(m => m.includes('not a member')));
});
it('fails the job on non-404 membership errors', async () => {
const { github, context, core } = createMocks({
payloadIssue: { user: { login: 'user1' } },
});
const serverError = new Error('Internal Server Error');
serverError.status = 500;
github.rest.teams.getMembershipForUserInOrg = async () => { throw serverError; };
await assert.rejects(
() => checkTeamMembership({ github, context, core, ...BASE_OPTS }),
(err) => err === serverError,
);
assert.ok(core._failedMessages.some(m => m.includes('membership lookup failed')));
});
it('fails the job on membership errors without status code', async () => {
const { github, context, core } = createMocks({
payloadIssue: { user: { login: 'user1' } },
});
const networkError = new Error('ECONNREFUSED');
github.rest.teams.getMembershipForUserInOrg = async () => { throw networkError; };
await assert.rejects(
() => checkTeamMembership({ github, context, core, ...BASE_OPTS }),
(err) => err === networkError,
);
assert.ok(core._failedMessages.some(m => m.includes('membership lookup failed')));
});
});
+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
+199
View File
@@ -0,0 +1,199 @@
name: Issue Triage
on:
workflow_dispatch:
inputs:
issue_number:
description: Issue number to triage
required: true
type: string
permissions:
contents: read
issues: write
id-token: write
concurrency:
group: issue-triage-${{ github.repository }}-${{ github.event.issue.number || inputs.issue_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 }}
issue_number: ${{ steps.issue.outputs.issue_number }}
repo: ${{ steps.issue.outputs.repo }}
steps:
- name: Resolve issue metadata
id: issue
shell: bash
env:
ISSUE_NUMBER_EVENT: ${{ github.event.issue.number }}
ISSUE_NUMBER_INPUT: ${{ inputs.issue_number }}
run: |
set -euo pipefail
if [[ "${GITHUB_EVENT_NAME}" == "issues" ]]; then
issue_number="${ISSUE_NUMBER_EVENT}"
else
issue_number="${ISSUE_NUMBER_INPUT}"
fi
if [[ ! "$issue_number" =~ ^[1-9][0-9]*$ ]]; then
echo "Could not determine issue number; for workflow_dispatch runs, the 'issue_number' input is required." >&2
exit 1
fi
echo "issue_number=${issue_number}" >> "$GITHUB_OUTPUT"
echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
- name: Checkout scripts
uses: actions/checkout@v6
with:
sparse-checkout: .github/scripts
fetch-depth: 1
persist-credentials: false
- name: Check issue author team membership
id: check
uses: actions/github-script@v8
env:
TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }}
ISSUE_NUMBER: ${{ steps.issue.outputs.issue_number }}
with:
github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
script: |
const checkTeamMembership = require('./.github/scripts/check_team_membership.js');
const { author, isTeamMember } = await checkTeamMembership({
github,
context,
core,
teamSlug: process.env.TEAM_NAME,
issueNumber: process.env.ISSUE_NUMBER,
});
core.setOutput('is_team_member', isTeamMember ? 'true' : 'false');
if (isTeamMember) {
core.info(`Author ${author} is a team member; skipping auto-triage.`);
} else {
core.info(`Author ${author} is not a team member; proceeding with triage.`);
}
triage:
runs-on: ubuntu-latest
needs: team_check
if: ${{ needs.team_check.outputs.is_team_member == 'false' }}
environment: integration
timeout-minutes: 60
steps:
# Safe checkout: base repo only.
- name: Checkout target repo base
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
path: target-repo
# Private DevFlow (maf-dashboard) checkout.
- 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: Azure CLI Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Classify issue relevance
id: spam
working-directory: ${{ env.DEVFLOW_PATH }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SK_REPO_PATH: ${{ env.TARGET_REPO_PATH }}
AGENT_REPO_PATH: ${{ env.TARGET_REPO_PATH }}
ISSUE_REPO: ${{ needs.team_check.outputs.repo }}
ISSUE_NUMBER: ${{ needs.team_check.outputs.issue_number }}
run: |
uv run python scripts/classify_issue_spam.py \
--repo "$ISSUE_REPO" \
--issue-number "$ISSUE_NUMBER" \
--repo-path "${TARGET_REPO_PATH}" \
--apply-labels
- name: Stop after spam gate
if: ${{ steps.spam.outputs.decision != 'allow' }}
shell: bash
env:
SPAM_DECISION: ${{ steps.spam.outputs.decision }}
run: |
echo "Stopping: spam gate decided: ${SPAM_DECISION}"
exit 1
- name: Reproduce reported issue
if: ${{ steps.spam.outputs.decision == 'allow' }}
id: repro
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 }}
ISSUE_REPO: ${{ needs.team_check.outputs.repo }}
ISSUE_NUMBER: ${{ needs.team_check.outputs.issue_number }}
# Model-provider settings for generated repro code. Never enter the
# agent prompt; consumed by SDK constructors via os.environ. Azure
# OpenAI and Foundry auth via AAD from the azure/login step above.
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_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
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 }}
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 || '' }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_CHAT_MODEL: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
run: |
uv run python scripts/trigger_issue_repro.py \
--repo "$ISSUE_REPO" \
--issue-number "$ISSUE_NUMBER" \
--github-username "$GITHUB_ACTOR"
+163 -1
View File
@@ -87,6 +87,14 @@ jobs:
-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:
@@ -130,6 +138,14 @@ jobs:
-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, Hyperlight, Ollama, MCP)
python-tests-misc-integration:
@@ -173,6 +189,14 @@ jobs:
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
--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
@@ -249,6 +273,14 @@ jobs:
-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
# Foundry integration tests
python-tests-foundry:
@@ -295,6 +327,61 @@ jobs:
-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
# Foundry Hosting integration tests
python-tests-foundry-hosting:
name: Python Integration Tests - Foundry Hosting
runs-on: ubuntu-latest
environment: integration
timeout-minutes: 60
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
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
id: python-setup
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Azure CLI Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Test with pytest (Foundry Hosting integration)
timeout-minutes: 15
run: >
uv run pytest --import-mode=importlib
packages/foundry_hosting/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-hosting
path: ./python/pytest.xml
if-no-files-found: ignore
# Azure Cosmos integration tests
python-tests-cosmos:
@@ -339,7 +426,81 @@ 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-foundry-hosting,
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()
@@ -352,6 +513,7 @@ jobs:
python-tests-misc-integration,
python-tests-functions,
python-tests-foundry,
python-tests-foundry-hosting,
python-tests-cosmos
]
steps:
+176 -1
View File
@@ -38,6 +38,7 @@ jobs:
miscChanged: ${{ steps.filter.outputs.misc }}
functionsChanged: ${{ steps.filter.outputs.functions }}
foundryChanged: ${{ steps.filter.outputs.foundry }}
foundryHostingChanged: ${{ steps.filter.outputs.foundry_hosting }}
cosmosChanged: ${{ steps.filter.outputs.cosmos }}
steps:
- uses: actions/checkout@v6
@@ -80,6 +81,8 @@ jobs:
- 'python/packages/foundry/**'
- 'python/samples/**/providers/foundry/**'
- 'python/samples/02-agents/embeddings/foundry_embeddings.py'
foundry_hosting:
- 'python/packages/foundry_hosting/**'
cosmos:
- 'python/packages/azure-cosmos/**'
# run only if 'python' files were changed
@@ -181,6 +184,13 @@ jobs:
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:
@@ -244,6 +254,13 @@ jobs:
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:
@@ -321,6 +338,13 @@ jobs:
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:
@@ -392,6 +416,13 @@ jobs:
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-foundry:
name: Python Integration Tests - Foundry
@@ -409,6 +440,10 @@ jobs:
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:
@@ -448,6 +483,74 @@ jobs:
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
# Foundry Hosting integration tests
python-tests-foundry-hosting:
name: Python Tests - Foundry Hosting Integration
needs: paths-filter
if: >
github.event_name != 'pull_request' &&
needs.paths-filter.outputs.pythonChanges == 'true' &&
(github.event_name != 'merge_group' ||
needs.paths-filter.outputs.foundryHostingChanged == 'true' ||
needs.paths-filter.outputs.coreChanged == 'true')
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- 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: Test with pytest (Foundry Hosting integration)
timeout-minutes: 15
run: >
uv run pytest --import-mode=importlib
packages/foundry_hosting/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/pytest.xml
summary: true
display-options: fEX
fail-on-empty: false
title: Foundry Hosting integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-foundry-hosting
path: ./python/pytest.xml
if-no-files-found: ignore
# TODO: Add python-tests-lab
@@ -497,7 +600,7 @@ 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 --junitxml=pytest.xml
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()
@@ -508,6 +611,77 @@ jobs:
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-foundry-hosting,
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()
@@ -520,6 +694,7 @@ jobs:
python-tests-misc-integration,
python-tests-functions,
python-tests-foundry,
python-tests-foundry-hosting,
python-tests-cosmos,
]
steps:
+4
View File
@@ -136,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/
+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/
+21 -16
View File
@@ -22,11 +22,16 @@
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.23" />
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.3" />
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.4" />
<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.Identity" Version="1.20.0" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
<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="1.6.0" />
<PackageVersion Include="Mscc.GenerativeAI.Microsoft" Version="2.9.3" />
@@ -37,29 +42,29 @@
<!-- System.* -->
<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="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.6" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.4" />
<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.4" />
<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.14.0" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.14.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.13.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.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" />
@@ -99,8 +104,8 @@
<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 -->
@@ -183,4 +188,4 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
</Project>
+53 -25
View File
@@ -1,12 +1,9 @@
<Solution>
<Solution>
<Configurations>
<BuildType Name="Debug" />
<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" />
@@ -67,6 +64,7 @@
<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" />
<Project Path="samples/02-agents/Agents/Agent_Step20_DynamicFunctionTools/Agent_Step20_DynamicFunctionTools.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/DeclarativeAgents/">
<Project Path="samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
@@ -162,6 +160,7 @@
<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" />
<Project Path="samples/02-agents/AgentsWithFoundry/Agent_Step25_ToolboxServerSideTools/Agent_Step25_ToolboxServerSideTools.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/Evaluation/">
<Project Path="samples/02-agents/Evaluation/Evaluation_SimpleEval/Evaluation_SimpleEval.csproj" />
@@ -283,7 +282,44 @@
<Folder Name="/Samples/03-workflows/Evaluation/">
<Project Path="samples/03-workflows/Evaluation/Evaluation_WorkflowEval/Evaluation_WorkflowEval.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/" />
<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" />
@@ -307,11 +343,13 @@
<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>
<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" />
@@ -338,15 +376,6 @@
<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/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" />
@@ -503,18 +532,19 @@
<File Path="tests/Directory.Build.props" />
</Folder>
<Folder Name="/src/">
<Project Path="src/Aspire.Hosting.AgentFramework.DevUI/Aspire.Hosting.AgentFramework.DevUI.csproj" />
<Project Path="src/Microsoft.Agents.AI.A2A/Microsoft.Agents.AI.A2A.csproj" />
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
<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.Foundry/Microsoft.Agents.AI.Foundry.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.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" />
@@ -536,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/Foundry.IntegrationTests/Foundry.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.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" />
@@ -557,12 +586,11 @@
<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.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.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.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" />
+1
View File
@@ -9,6 +9,7 @@
"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.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",
+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>
+7 -5
View File
@@ -1,13 +1,14 @@
<Project>
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.1.0</VersionPrefix>
<VersionPrefix>1.3.0</VersionPrefix>
<RCNumber>1</RCNumber>
<DateSuffix>260423</DateSuffix>
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260410.1</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260410.1</PackageVersion>
<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.1.0</GitTag>
<GitTag>1.3.0</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
@@ -29,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>
@@ -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
```
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="A2A" />
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup>
<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
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,281 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to dynamically expand the set of function tools available to an
// agent during a function-calling loop. The agent starts with a single "RequestTools" function.
// When the model calls RequestTools with a description of the capabilities needed, the function
// uses the ambient FunctionInvocationContext to add new tools to ChatOptions.Tools. The agent
// can then use the newly added tools in subsequent iterations of the same function-calling loop.
using System.ComponentModel;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
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-5.4-mini";
// Pre-defined tool implementations that can be loaded on demand.
[Description("Get the current weather for a city.")]
static string GetWeather([Description("The city name.")] string city) =>
city.ToUpperInvariant() switch
{
"SEATTLE" => "Seattle: 55°F, cloudy with light rain.",
"NEW YORK" => "New York: 72°F, sunny and warm.",
"LONDON" => "London: 48°F, overcast with fog.",
_ => $"{city}: weather data not available, please provide one of the following city names: 'Seattle', 'New York', 'London'."
};
[Description("Get the current local time for a city.")]
static string GetTime([Description("The city name.")] string city) =>
city.ToUpperInvariant() switch
{
"SEATTLE" => "Seattle: 9:00 AM PST",
"NEW YORK" => "New York: 12:00 PM EST",
"LONDON" => "London: 5:00 PM GMT",
_ => $"{city}: time data not available, please provide one of the following city names: 'Seattle', 'New York', 'London'."
};
[Description("Convert a temperature from Fahrenheit to Celsius.")]
static string ConvertFahrenheitToCelsius([Description("The temperature in Fahrenheit.")] double fahrenheit) =>
$"{fahrenheit}°F = {(fahrenheit - 32) * 5 / 9:F1}°C";
// A registry of tool sets that can be loaded by description keyword.
Dictionary<string, List<AITool>> toolCatalog = new(StringComparer.OrdinalIgnoreCase)
{
["weather"] = [AIFunctionFactory.Create(GetWeather, name: "GetWeather")],
["time"] = [AIFunctionFactory.Create(GetTime, name: "GetTime")],
["temperature"] = [AIFunctionFactory.Create(ConvertFahrenheitToCelsius, name: "ConvertFahrenheitToCelsius")],
};
// The RequestTools function uses the ambient FunctionInvocationContext to add tools dynamically.
AIFunction requestToolsFunction = AIFunctionFactory.Create(
[Description("Request additional tools to be loaded based on a description of the functionality needed. " +
"Call this when you need capabilities that are not yet available in your current tool set.")] (
[Description("A description of the functionality required, e.g. 'weather', 'time', or 'temperature conversion'.")] string description
) =>
{
// Access the ambient FunctionInvocationContext provided by FunctionInvokingChatClient.
var context = FunctionInvokingChatClient.CurrentContext
?? throw new InvalidOperationException("No ambient FunctionInvocationContext available.");
var tools = context.Options?.Tools;
if (tools is null)
{
return "Unable to register new tools: ChatOptions.Tools is not available.";
}
// Find matching tool sets from the catalog.
List<string> addedToolNames = [];
foreach (var kvp in toolCatalog)
{
var keyword = kvp.Key;
var catalogTools = kvp.Value;
if (description.Contains(keyword, StringComparison.OrdinalIgnoreCase))
{
foreach (var tool in catalogTools)
{
// Avoid adding duplicates.
if (tool is AIFunction fn && !tools.Any(t => t is AIFunction existing && existing.Name == fn.Name))
{
tools.Add(tool);
addedToolNames.Add(fn.Name);
}
}
}
}
return addedToolNames.Count > 0
? "Successfully loaded tools"
: $"No tools matched the description '{description}'. Available categories: {string.Join(", ", toolCatalog.Keys)}.";
},
name: "RequestTools");
// Create the agent with only the RequestTools function initially.
// Insert chat client middleware that logs the tools available on each LLM call,
// making the dynamic expansion visible in the console output.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName)
.AsIChatClient()
.AsBuilder()
.Use(getResponseFunc: ToolLoggingMiddleware, getStreamingResponseFunc: ToolLoggingStreamingMiddleware)
.BuildAIAgent(
instructions: """
You are a helpful assistant. You start with limited tools.
When you need functionality that you don't currently have, call RequestTools with a description
of what you need. After new tools are loaded, use them to answer the user's question.
""",
tools: [requestToolsFunction]);
// Run a conversation that triggers dynamic tool expansion.
Console.WriteLine("=== Dynamic Function Tools Sample ===\n");
string[] prompts =
[
"What's the weather like in Seattle and London?",
"What time is it in New York?",
"Can you convert those temperatures to Celsius?"
];
// --- Non-Streaming Mode ---
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("=== Non-Streaming Mode ===");
Console.ResetColor();
Console.WriteLine();
AgentSession session = await agent.CreateSessionAsync();
foreach (var prompt in prompts)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("[User] ");
Console.ResetColor();
Console.WriteLine(prompt);
var response = await agent.RunAsync(prompt, session);
// Print all message contents including tool calls, tool results, and text.
foreach (var message in response.Messages)
{
foreach (var content in message.Contents)
{
switch (content)
{
case FunctionCallContent functionCall:
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($" [Tool Call] {functionCall.Name}({string.Join(", ", functionCall.Arguments?.Select(a => $"{a.Key}: {a.Value}") ?? [])})");
Console.ResetColor();
break;
case FunctionResultContent functionResult:
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($" [Tool Result] {functionResult.CallId} => {functionResult.Result}");
Console.ResetColor();
break;
case TextContent textContent when !string.IsNullOrWhiteSpace(textContent.Text):
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write("[Agent] ");
Console.ResetColor();
Console.WriteLine(textContent.Text);
break;
}
}
}
Console.WriteLine();
}
// --- Streaming Mode ---
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("=== Streaming Mode ===");
Console.ResetColor();
Console.WriteLine();
AgentSession streamingSession = await agent.CreateSessionAsync();
foreach (var prompt in prompts)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("[User] ");
Console.ResetColor();
Console.WriteLine(prompt);
bool inAgentText = false;
await foreach (var update in agent.RunStreamingAsync(prompt, streamingSession))
{
foreach (var content in update.Contents)
{
switch (content)
{
case FunctionCallContent functionCall:
if (inAgentText)
{
Console.WriteLine();
inAgentText = false;
}
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($" [Tool Call] {functionCall.Name}({string.Join(", ", functionCall.Arguments?.Select(a => $"{a.Key}: {a.Value}") ?? [])})");
Console.ResetColor();
break;
case FunctionResultContent functionResult:
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($" [Tool Result] {functionResult.CallId} => {functionResult.Result}");
Console.ResetColor();
break;
case TextContent textContent when !string.IsNullOrWhiteSpace(textContent.Text):
if (!inAgentText)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write("[Agent] ");
Console.ResetColor();
inAgentText = true;
}
Console.Write(textContent.Text);
break;
}
}
}
if (inAgentText)
{
Console.WriteLine();
}
Console.WriteLine();
}
// Chat client middleware that logs the number and names of tools on each LLM request.
async Task<ChatResponse> ToolLoggingMiddleware(
IEnumerable<ChatMessage> messages,
ChatOptions? options,
IChatClient innerChatClient,
CancellationToken cancellationToken)
{
LogTools(options);
return await innerChatClient.GetResponseAsync(messages, options, cancellationToken);
}
// Streaming version of the tool logging middleware.
async IAsyncEnumerable<ChatResponseUpdate> ToolLoggingStreamingMiddleware(
IEnumerable<ChatMessage> messages,
ChatOptions? options,
IChatClient innerChatClient,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
LogTools(options);
await foreach (var update in innerChatClient.GetStreamingResponseAsync(messages, options, cancellationToken))
{
yield return update;
}
}
// Shared helper to log the current tool set.
void LogTools(ChatOptions? options)
{
if (options?.Tools is { Count: > 0 } tools)
{
var toolNames = tools.OfType<AIFunction>().Select(t => t.Name);
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" [Middleware] LLM call with {tools.Count} tool(s): {string.Join(", ", toolNames)}");
Console.ResetColor();
}
else
{
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine(" [Middleware] LLM call with 0 tools");
Console.ResetColor();
}
}
@@ -0,0 +1,38 @@
# Dynamic Function Tools
This sample demonstrates how to dynamically expand the set of function tools available to an agent during a function-calling loop.
## What it demonstrates
- The agent starts with only a single `RequestTools` function
- When the model needs capabilities it doesn't have, it calls `RequestTools` with a description of the functionality needed
- The `RequestTools` function uses the ambient `FunctionInvokingChatClient.CurrentContext` to access `ChatOptions.Tools` and add new tools at runtime
- The agent then uses the newly added tools in subsequent iterations of the same function-calling loop
## How it works
1. A tool catalog maps keywords (e.g. "weather", "time", "temperature") to pre-built `AIFunction` instances
2. The `RequestTools` function matches the description against catalog keywords and adds matching tools to `ChatOptions.Tools`
3. `FunctionInvokingChatClient` automatically picks up the new tools on the next iteration of its loop
## Prerequisites
- .NET 10 SDK or later
- Azure OpenAI service endpoint and deployment configured
- Azure CLI installed and authenticated (for Azure credential authentication)
- User has the `Cognitive Services OpenAI Contributor` role for the Azure OpenAI resource
## Running the sample
Set the required environment variables:
```powershell
$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
```
Run the sample:
```powershell
dotnet run
```
@@ -46,6 +46,7 @@ Before you begin, ensure you have the following prerequisites:
|[Providing additional AI Context to an agent using multiple AIContextProviders](./Agent_Step17_AdditionalAIContext/)|This sample demonstrates how to inject additional AI context into a ChatClientAgent using multiple custom AIContextProvider components that are attached to the agent.|
|[Using compaction pipeline with an agent](./Agent_Step18_CompactionPipeline/)|This sample demonstrates how to use a compaction pipeline to efficiently limit the size of the conversation history for an agent.|
|[In-function-loop checkpointing](./Agent_Step19_InFunctionLoopCheckpointing/)|This sample demonstrates how to persist chat history after each service call during a tool-calling loop, enabling crash recovery and mid-run observability.|
|[Dynamic function tools](./Agent_Step20_DynamicFunctionTools/)|This sample demonstrates how to dynamically expand the set of function tools available to an agent during a function-calling loop using the ambient FunctionInvocationContext.|
## Running the samples from the console
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<PackageReference Include="Azure.AI.Projects" VersionOverride="2.1.0-beta.1" />
</ItemGroup>
</Project>
@@ -0,0 +1,148 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to load a Foundry toolbox and pass its tools as server-side
// tools when creating an agent. The Foundry platform handles tool execution — the agent
// process does not invoke tools locally.
using System.ClientModel;
using System.ClientModel.Primitives;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
#pragma warning disable OPENAI001 // Experimental API
#pragma warning disable AAIP001 // AgentToolboxes is experimental
#pragma warning disable CS8321 // Local functions may be commented-out alternatives
// Replace with your own Foundry toolbox name.
const string ToolboxName = "research_toolbox";
// Used only by CombineToolboxes — swap in a second toolbox you own.
const string SecondToolboxName = "analysis_toolbox";
// Replace with any question that exercises the tools configured in your toolbox.
const string Query = "Introduce yourself and briefly describe the tools you can use to help me.";
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("Set FOUNDRY_PROJECT_ENDPOINT to your Foundry project endpoint.");
string model = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "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
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
var projectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
await Main(projectClient, model, endpoint);
// await CombineToolboxes(projectClient, model, endpoint);
// ---------------------------------------------------------------------------
// Main: single toolbox
// ---------------------------------------------------------------------------
static async Task Main(AIProjectClient projectClient, string model, string endpoint)
{
Console.WriteLine("=== Foundry Toolbox Server-Side Tools Example ===");
// Comment out if the toolbox already exists in your Foundry project.
await CreateSampleToolboxAsync(ToolboxName, endpoint);
// Omit the version to resolve the toolbox's current default version at runtime.
var tools = await projectClient.GetToolboxToolsAsync(ToolboxName);
AIAgent agent = projectClient
.AsAIAgent(
model: model,
instructions: "You are a research assistant. Use the available tools to answer questions.",
tools: tools.ToList());
Console.WriteLine($"User: {Query}");
Console.WriteLine($"Result: {await agent.RunAsync(Query)}\n");
}
// ---------------------------------------------------------------------------
// Alternative: combine tools from multiple toolboxes
// ---------------------------------------------------------------------------
static async Task CombineToolboxes(AIProjectClient projectClient, string model, string endpoint)
{
Console.WriteLine("=== Combine Toolboxes Example ===");
// Comment out if the toolboxes already exist in your Foundry project.
await CreateSampleToolboxAsync(ToolboxName, endpoint);
await CreateSampleToolboxAsync(SecondToolboxName, endpoint);
var toolboxA = await projectClient.GetToolboxToolsAsync(ToolboxName);
var toolboxB = await projectClient.GetToolboxToolsAsync(SecondToolboxName);
var allTools = toolboxA.Concat(toolboxB).ToList();
AIAgent agent = projectClient
.AsAIAgent(
model: model,
instructions: "You are a research assistant. Use all available tools to answer questions.",
tools: allTools);
Console.WriteLine($"User: {Query}");
Console.WriteLine($"Combined-toolbox result: {await agent.RunAsync(Query)}\n");
}
// ---------------------------------------------------------------------------
// Helper: create (or replace) a sample toolbox so the sample works out-of-the-box
// ---------------------------------------------------------------------------
static async Task CreateSampleToolboxAsync(string name, string endpoint)
{
// Toolboxes are normally configured in the Foundry portal or a deployment
// script, not the application itself. This helper exists so the sample can
// be run end-to-end without first setting a toolbox up by hand.
// The Foundry-Features header is currently required for toolbox CRUD operations.
var options = new AgentAdministrationClientOptions();
options.AddPolicy(new FoundryFeaturesPolicy("Toolboxes=V1Preview"), PipelinePosition.PerCall);
var adminClient = new AgentAdministrationClient(
new Uri(endpoint),
new DefaultAzureCredential(),
options);
var toolboxClient = adminClient.GetAgentToolboxes();
// Delete existing toolbox if present (ignore 404).
try
{
await toolboxClient.DeleteToolboxAsync(name);
Console.WriteLine($"Deleted existing toolbox '{name}'");
}
catch (ClientResultException ex) when (ex.Status == 404)
{
// Toolbox does not exist — nothing to delete.
}
// Create a fresh version with a single MCP tool.
ProjectsAgentTool mcpTool = ProjectsAgentTool.AsProjectTool(ResponseTool.CreateMcpTool(
serverLabel: "api-specs",
serverUri: new Uri("https://gitmcp.io/Azure/azure-rest-api-specs"),
toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)));
var created = (await toolboxClient.CreateToolboxVersionAsync(
name: name,
tools: [mcpTool],
description: "Sample toolbox with an MCP tool — created by Agent_Step25 sample.")).Value;
Console.WriteLine($"Created toolbox '{created.Name}' v{created.Version} ({created.Tools.Count} tool(s))");
}
// ---------------------------------------------------------------------------
// Pipeline policy that adds the Foundry-Features header for toolbox CRUD
// ---------------------------------------------------------------------------
internal sealed class FoundryFeaturesPolicy(string feature) : PipelinePolicy
{
private const string FeatureHeader = "Foundry-Features";
public override void Process(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
{
message.Request.Headers.Add(FeatureHeader, feature);
ProcessNext(message, pipeline, currentIndex);
}
public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
{
message.Request.Headers.Add(FeatureHeader, feature);
return ProcessNextAsync(message, pipeline, currentIndex);
}
}
@@ -0,0 +1,46 @@
# Agent_Step25_ToolboxServerSideTools
This sample demonstrates loading a named Foundry toolbox and passing its tools as
**server-side tools** when creating an agent via `AsAIAgent()`.
When tools from a toolbox are passed this way, they are sent as tool definitions in
the Responses API request. The Foundry platform handles tool execution — the agent
process does not invoke tools locally.
This is the dotnet equivalent of the Python sample:
`python/samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox.py`
## Prerequisites
- A Microsoft Foundry project
- `AZURE_AI_PROJECT_ENDPOINT` environment variable set to your Foundry project endpoint
- `AZURE_AI_MODEL_DEPLOYMENT_NAME` environment variable set (defaults to `gpt-5.4-mini`)
The sample recreates the toolbox on each run, replacing any existing toolbox with
the same name. Comment out the `CreateSampleToolboxAsync` call if you want to keep
an existing toolbox unchanged.
## How it works
1. `projectClient.GetToolboxVersionAsync(name)` fetches the toolbox definition from the
Foundry project API (resolving the default version if none is specified)
2. `ToolboxVersion.ToAITools()` converts each tool definition to an `AITool` instance
3. The tools are passed to `AsAIAgent(tools: ...)` which includes them in the Responses
API request as server-side tool definitions
For a one-liner, use `projectClient.GetToolboxToolsAsync(name)` to fetch and convert in one call.
## Sample flows
| Flow | Description |
|------|-------------|
| `Main` (default) | Loads a single toolbox and runs an agent with its tools |
| `CombineToolboxes` | Loads two toolboxes and merges their tools into one agent |
Uncomment the desired flow in the top-level statements to try each one.
## Running the sample
```bash
dotnet run
```
+1
View File
@@ -19,3 +19,4 @@ The getting started samples demonstrate the fundamental concepts and functionali
| [Declarative Agents](./DeclarativeAgents) | Loading and executing AI agents from YAML configuration files |
| [AG-UI](./AGUI/README.md) | Getting started with AG-UI (Agent UI Protocol) servers and clients |
| [Dev UI](./DevUI/README.md) | Interactive web interface for testing and debugging AI agents during development |
| [A2A Agents](./A2A/README.md) | Working with Agent-to-Agent (A2A) specific features |
+1
View File
@@ -0,0 +1 @@
**/Properties/launchSettings.json
@@ -0,0 +1,2 @@
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
@@ -0,0 +1,17 @@
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedInvocationsEchoAgent.dll"]
@@ -0,0 +1,19 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI.Abstractions source,
# which means a standard multi-stage Docker build cannot resolve dependencies outside
# this folder. Instead, pre-publish the app targeting the container runtime and copy
# the output into the container:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-invocations-echo-agent .
# docker run --rm -p 8088:8088 hosted-invocations-echo-agent
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedInvocationsEchoAgent.dll"]
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// A minimal <see cref="AIAgent"/> that echoes the user's input text back as the response.
/// No LLM or external service is required.
/// </summary>
public sealed class EchoAIAgent : AIAgent
{
/// <inheritdoc/>
public override string Name => "echo-agent";
/// <inheritdoc/>
public override string Description => "An agent that echoes back the input message.";
/// <inheritdoc/>
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
var inputText = GetInputText(messages);
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, $"Echo: {inputText}"));
return Task.FromResult(response);
}
/// <inheritdoc/>
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var inputText = GetInputText(messages);
yield return new AgentResponseUpdate
{
Role = ChatRole.Assistant,
Contents = [new TextContent($"Echo: {inputText}")],
};
await Task.CompletedTask;
}
/// <inheritdoc/>
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
=> new(new EchoAgentSession());
/// <inheritdoc/>
protected override ValueTask<JsonElement> SerializeSessionCoreAsync(
AgentSession session,
JsonSerializerOptions? jsonSerializerOptions = null,
CancellationToken cancellationToken = default)
=> new(JsonSerializer.SerializeToElement(new { }, jsonSerializerOptions));
/// <inheritdoc/>
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(
JsonElement serializedState,
JsonSerializerOptions? jsonSerializerOptions = null,
CancellationToken cancellationToken = default)
=> new(new EchoAgentSession());
private static string GetInputText(IEnumerable<ChatMessage> messages)
{
foreach (var message in messages)
{
if (message.Role == ChatRole.User)
{
return message.Text ?? string.Empty;
}
}
return string.Empty;
}
/// <summary>
/// Minimal session for the echo agent. No state is persisted.
/// </summary>
private sealed class EchoAgentSession : AgentSession;
}
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.AgentServer.Invocations;
using Microsoft.Agents.AI;
namespace HostedInvocationsEchoAgent;
/// <summary>
/// An <see cref="InvocationHandler"/> that reads the request body as plain text,
/// passes it to the <see cref="EchoAIAgent"/>, and writes the response back.
/// </summary>
public sealed class EchoInvocationHandler(EchoAIAgent agent) : InvocationHandler
{
/// <inheritdoc/>
public override async Task HandleAsync(
HttpRequest request,
HttpResponse response,
InvocationContext context,
CancellationToken cancellationToken)
{
// Read the raw text from the request body.
using var reader = new StreamReader(request.Body);
var input = await reader.ReadToEndAsync(cancellationToken);
// Run the echo agent with the input text.
var agentResponse = await agent.RunAsync(input, cancellationToken: cancellationToken);
// Write the agent response text back to the HTTP response.
response.ContentType = "text/plain";
await response.WriteAsync(agentResponse.Text, cancellationToken);
}
}
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedInvocationsEchoAgent</RootNamespace>
<AssemblyName>HostedInvocationsEchoAgent</AssemblyName>
<NoWarn>$(NoWarn);</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.Invocations" />
<PackageReference Include="DotNetEnv" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
</ItemGroup>
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Abstractions" Version="1.0.0" />
<PackageReference Include="Azure.AI.AgentServer.Invocations" />
</ItemGroup>
-->
</Project>
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.AgentServer.Invocations;
using DotNetEnv;
using HostedInvocationsEchoAgent;
using Microsoft.Agents.AI;
// Load .env file if present (for local development)
Env.TraversePath().Load();
var builder = WebApplication.CreateBuilder(args);
// Register the echo agent as a singleton (no LLM needed).
builder.Services.AddSingleton<EchoAIAgent>();
// Register the Invocations SDK services and wire the handler.
builder.Services.AddInvocationsServer();
builder.Services.AddScoped<InvocationHandler, EchoInvocationHandler>();
var app = builder.Build();
// Map the Invocations protocol endpoints:
// POST /invocations — invoke the agent
// GET /invocations/{id} — get result (not used by this sample)
// POST /invocations/{id}/cancel — cancel (not used by this sample)
app.MapInvocationsServer();
app.Run();
@@ -0,0 +1,76 @@
# Hosted-Invocations-EchoAgent
A minimal echo agent hosted as a Foundry Hosted Agent using the **Invocations protocol**. The agent reads the request body as plain text, passes it through a custom `EchoAIAgent`, and writes the echoed text back in the response. No LLM or Azure credentials are required.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
## Configuration
Copy the template:
```bash
cp .env.example .env
```
> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
## Running directly (contributors)
This project uses `ProjectReference` to build against the local Agent Framework source.
```bash
cd dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent
dotnet run
```
The agent will start on `http://localhost:8088`.
### Test it
```bash
curl -X POST http://localhost:8088/invocations \
-H "Content-Type: text/plain" \
-d "Hello, world!"
```
Expected response:
```
Echo: Hello, world!
```
## Running with Docker
Since this project uses `ProjectReference`, the standard `Dockerfile` cannot resolve dependencies outside this folder. Use `Dockerfile.contributor` which takes a pre-published output.
### 1. Publish for the container runtime (Linux Alpine)
```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
```
### 2. Build the Docker image
```bash
docker build -f Dockerfile.contributor -t hosted-invocations-echo-agent .
```
### 3. Run the container
```bash
docker run --rm -p 8088:8088 hosted-invocations-echo-agent
```
### 4. Test it
```bash
curl -X POST http://localhost:8088/invocations \
-H "Content-Type: text/plain" \
-d "Hello from Docker!"
```
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `Hosted-Invocations-EchoAgent.csproj` for the `PackageReference` alternative.
@@ -0,0 +1,27 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-invocations-echo-agent
displayName: "Hosted Invocations Echo Agent"
description: >
A minimal echo agent hosted as a Foundry Hosted Agent using the Invocations
protocol. Reads the request body as plain text, echoes it back in the response.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Invocations Protocol
- Agent Framework
template:
name: hosted-invocations-echo-agent
kind: hosted
protocols:
- protocol: invocations
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
parameters:
properties: []
resources: []
@@ -0,0 +1,9 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-invocations-echo-agent
protocols:
- protocol: invocations
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
@@ -0,0 +1,129 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI;
/// <summary>
/// An <see cref="AIAgent"/> that invokes a remote agent hosted with the Invocations protocol
/// by sending plain-text HTTP POST requests to the <c>/invocations</c> endpoint.
/// </summary>
public sealed class InvocationsAIAgent : AIAgent
{
private readonly HttpClient _httpClient;
private readonly Uri _invocationsUri;
/// <summary>
/// Initializes a new instance of the <see cref="InvocationsAIAgent"/> class.
/// </summary>
/// <param name="agentEndpoint">
/// The base URI of the hosted agent (e.g., <c>http://localhost:8089</c>).
/// The <c>/invocations</c> path is appended automatically.
/// </param>
/// <param name="httpClient">Optional <see cref="HttpClient"/> to use. If <see langword="null"/>, a new instance is created.</param>
/// <param name="name">Optional name for the agent.</param>
/// <param name="description">Optional description for the agent.</param>
public InvocationsAIAgent(
Uri agentEndpoint,
HttpClient? httpClient = null,
string? name = null,
string? description = null)
{
ArgumentNullException.ThrowIfNull(agentEndpoint);
this._httpClient = httpClient ?? new HttpClient();
// Ensure the base URI ends with a slash so that combining works correctly.
var baseUri = agentEndpoint.AbsoluteUri.EndsWith('/')
? agentEndpoint
: new Uri(agentEndpoint.AbsoluteUri + "/");
this._invocationsUri = new Uri(baseUri, "invocations");
this.Name = name ?? "invocations-agent";
this.Description = description ?? "An agent that calls a remote Invocations protocol endpoint.";
}
/// <inheritdoc/>
public override string? Name { get; }
/// <inheritdoc/>
public override string? Description { get; }
/// <inheritdoc/>
protected override async Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
var inputText = GetLastUserText(messages);
var responseText = await this.SendInvocationAsync(inputText, cancellationToken).ConfigureAwait(false);
return new AgentResponse(new ChatMessage(ChatRole.Assistant, responseText));
}
/// <inheritdoc/>
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// The Invocations protocol returns a complete response (no SSE streaming),
// so we yield a single update with the full text.
var inputText = GetLastUserText(messages);
var responseText = await this.SendInvocationAsync(inputText, cancellationToken).ConfigureAwait(false);
yield return new AgentResponseUpdate
{
Role = ChatRole.Assistant,
Contents = [new TextContent(responseText)],
};
}
/// <inheritdoc/>
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
=> new(new InvocationsAgentSession());
/// <inheritdoc/>
protected override ValueTask<JsonElement> SerializeSessionCoreAsync(
AgentSession session,
JsonSerializerOptions? jsonSerializerOptions = null,
CancellationToken cancellationToken = default)
=> new(JsonSerializer.SerializeToElement(new { }, jsonSerializerOptions));
/// <inheritdoc/>
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(
JsonElement serializedState,
JsonSerializerOptions? jsonSerializerOptions = null,
CancellationToken cancellationToken = default)
=> new(new InvocationsAgentSession());
private async Task<string> SendInvocationAsync(string input, CancellationToken cancellationToken)
{
using var content = new StringContent(input, System.Text.Encoding.UTF8, "text/plain");
using var response = await this._httpClient.PostAsync(this._invocationsUri, content, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
}
private static string GetLastUserText(IEnumerable<ChatMessage> messages)
{
string? lastUserText = null;
foreach (var message in messages)
{
if (message.Role == ChatRole.User)
{
lastUserText = message.Text;
}
}
return lastUserText ?? string.Empty;
}
/// <summary>
/// Minimal session for the invocations agent. No state is persisted.
/// </summary>
private sealed class InvocationsAgentSession : AgentSession;
}
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All rights reserved.
using DotNetEnv;
using Microsoft.Agents.AI;
// Load .env file if present (for local development)
Env.TraversePath().Load();
Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT")
?? "http://localhost:8088");
// Create an agent that calls the remote Invocations endpoint.
InvocationsAIAgent agent = new(agentEndpoint);
// REPL
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"""
══════════════════════════════════════════════════════════
Simple Invocations Agent Sample
Connected to: {agentEndpoint}
Type a message or 'quit' to exit
══════════════════════════════════════════════════════════
""");
Console.ResetColor();
Console.WriteLine();
while (true)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("You> ");
Console.ResetColor();
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input)) { continue; }
if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; }
try
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write("Agent> ");
Console.ResetColor();
await foreach (var update in agent.RunStreamingAsync(input))
{
Console.Write(update);
}
Console.WriteLine();
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Error: {ex.Message}");
Console.ResetColor();
}
Console.WriteLine();
}
Console.WriteLine("Goodbye!");
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>SimpleInvocationsAgentClient</RootNamespace>
<AssemblyName>simple-invocations-agent-client</AssemblyName>
<NoWarn>$(NoWarn);NU1605</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNetEnv" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,6 @@
AZURE_AI_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
AGENT_NAME=hosted-chat-client-agent
AZURE_BEARER_TOKEN=DefaultAzureCredential
@@ -0,0 +1,17 @@
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedChatClientAgent.dll"]
@@ -0,0 +1,19 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
# which means a standard multi-stage Docker build cannot resolve dependencies outside
# this folder. Instead, pre-publish the app targeting the container runtime and copy
# the output into the container:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-chat-client-agent .
# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-chat-client-agent --env-file .env hosted-chat-client-agent
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedChatClientAgent.dll"]
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedChatClientAgent</RootNamespace>
<AssemblyName>HostedChatClientAgent</AssemblyName>
<NoWarn>$(NoWarn);</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNetEnv" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
</ItemGroup>
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
-->
</Project>
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Projects;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
// Load .env file if present (for local development)
Env.TraversePath().Load();
var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."));
var agentName = Environment.GetEnvironmentVariable("AGENT_NAME")
?? throw new InvalidOperationException("AGENT_NAME is not set.");
var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity running in foundry).
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());
// Create the agent via the AI project client using the Responses API.
AIAgent agent = new AIProjectClient(projectEndpoint, credential)
.AsAIAgent(
model: deployment,
instructions: """
You are a helpful AI assistant hosted as a Foundry Hosted Agent.
You can help with a wide range of tasks including answering questions,
providing explanations, brainstorming ideas, and offering guidance.
Be concise, clear, and helpful in your responses.
""",
name: agentName,
description: "A simple general-purpose AI assistant");
// Host the agent as a Foundry Hosted Agent using the Responses API.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
var app = builder.Build();
app.MapFoundryResponses();
// In Development, also map the OpenAI-compatible route that AIProjectClient uses.
if (app.Environment.IsDevelopment())
{
app.MapFoundryResponses("openai/v1");
}
app.Run();
/// <summary>
/// A <see cref="TokenCredential"/> for local Docker debugging only.
///
/// When debugging and testing a hosted agent in a local Docker container, Azure CLI
/// and other interactive credentials are not available. This credential reads a
/// pre-fetched bearer token from the <c>AZURE_BEARER_TOKEN</c> environment variable.
///
/// This should NOT be used in production — tokens expire (~1 hour) and cannot be refreshed.
/// In production, the Foundry platform injects a managed identity automatically.
///
/// Generate a token on your host and pass it to the container:
/// export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
/// docker run -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN ...
/// </summary>
internal sealed class DevTemporaryTokenCredential : TokenCredential
{
private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
private readonly string? _token;
public DevTemporaryTokenCredential()
{
this._token = Environment.GetEnvironmentVariable(EnvironmentVariable);
}
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return this.GetAccessToken();
}
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return new ValueTask<AccessToken>(this.GetAccessToken());
}
private AccessToken GetAccessToken()
{
if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential")
{
throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
}
return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1));
}
}
@@ -0,0 +1,109 @@
# Hosted-ChatClientAgent
A simple general-purpose AI assistant hosted as a Foundry Hosted Agent using the Agent Framework instance hosting pattern. The agent is created inline via `AIProjectClient.AsAIAgent(model, instructions)` and served using the Responses protocol.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`)
- Azure CLI logged in (`az login`)
## Configuration
Copy the template and fill in your project endpoint:
```bash
cp .env.example .env
```
Edit `.env` and set your Azure AI Foundry project endpoint:
```env
AZURE_AI_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
```
> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
## Running directly (contributors)
This project uses `ProjectReference` to build against the local Agent Framework source.
```bash
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent
dotnet run
```
The agent will start on `http://localhost:8088`.
### Test it
Using the Azure Developer CLI:
```bash
azd ai agent invoke --local "Hello!"
```
Or with curl (specifying the agent name explicitly):
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "Hello!", "model": "hosted-chat-client-agent"}'
```
## Running with Docker
Since this project uses `ProjectReference`, the standard `Dockerfile` cannot resolve dependencies outside this folder. Use `Dockerfile.contributor` which takes a pre-published output.
### 1. Publish for the container runtime (Linux Alpine)
```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
```
### 2. Build the Docker image
```bash
docker build -f Dockerfile.contributor -t hosted-chat-client-agent .
```
### 3. Run the container
Generate a bearer token on your host and pass it to the container:
```bash
# Generate token (expires in ~1 hour)
export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
# Run with token
docker run --rm -p 8088:8088 \
-e AGENT_NAME=hosted-chat-client-agent \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
--env-file .env \
hosted-chat-client-agent
```
> **Note:** `AGENT_NAME` is passed via `-e` to simulate the platform injection. `AZURE_BEARER_TOKEN` provides Azure credentials to the container (tokens expire after ~1 hour). The `.env` file provides the remaining configuration.
### 4. Test it
Using the Azure Developer CLI:
```bash
azd ai agent invoke --local "Hello!"
```
Or with curl (specifying the agent name explicitly):
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "Hello!", "model": "hosted-chat-client-agent"}'
```
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor` — it performs a full `dotnet restore` and `dotnet publish` inside the container. See the commented section in `HostedChatClientAgent.csproj` for the `PackageReference` alternative.
@@ -0,0 +1,28 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-chat-client-agent
displayName: "Hosted Chat Client Agent"
description: >
A simple general-purpose AI assistant hosted as a Foundry Hosted Agent
using the Agent Framework instance hosting pattern.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Streaming
- Agent Framework
template:
name: hosted-chat-client-agent
kind: hosted
protocols:
- protocol: responses
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
parameters:
properties: []
resources: []
@@ -0,0 +1,9 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-chat-client-agent
protocols:
- protocol: responses
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
@@ -0,0 +1,5 @@
AZURE_AI_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
AGENT_NAME=<your-foundry-agent-name>
AZURE_BEARER_TOKEN=DefaultAzureCredential
@@ -0,0 +1,17 @@
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedFoundryAgent.dll"]
@@ -0,0 +1,19 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
# which means a standard multi-stage Docker build cannot resolve dependencies outside
# this folder. Instead, pre-publish the app targeting the container runtime and copy
# the output into the container:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-foundry-agent .
# docker run --rm -p 8088:8088 -e AGENT_NAME=<your-agent> -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-foundry-agent
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedFoundryAgent.dll"]
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedFoundryAgent</RootNamespace>
<AssemblyName>HostedFoundryAgent</AssemblyName>
<NoWarn>$(NoWarn);</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNetEnv" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
</ItemGroup>
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
-->
</Project>
@@ -0,0 +1,91 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Microsoft.Agents.AI.Foundry;
using Microsoft.Agents.AI.Foundry.Hosting;
// Load .env file if present (for local development)
Env.TraversePath().Load();
var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."));
var agentName = Environment.GetEnvironmentVariable("AGENT_NAME")
?? throw new InvalidOperationException("AGENT_NAME is not set.");
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity running in foundry).
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());
var aiProjectClient = new AIProjectClient(projectEndpoint, credential);
// Retrieve the Foundry-managed agent by name (latest version).
ProjectsAgentRecord agentRecord = await aiProjectClient
.AgentAdministrationClient.GetAgentAsync(agentName);
FoundryAgent agent = aiProjectClient.AsAIAgent(agentRecord);
// Host the agent as a Foundry Hosted Agent using the Responses API.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
var app = builder.Build();
app.MapFoundryResponses();
// In Development, also map the OpenAI-compatible route that AIProjectClient uses.
if (app.Environment.IsDevelopment())
{
app.MapFoundryResponses("openai/v1");
}
app.Run();
/// <summary>
/// A <see cref="TokenCredential"/> for local Docker debugging only.
///
/// When debugging and testing a hosted agent in a local Docker container, Azure CLI
/// and other interactive credentials are not available. This credential reads a
/// pre-fetched bearer token from the <c>AZURE_BEARER_TOKEN</c> environment variable.
///
/// This should NOT be used in production — tokens expire (~1 hour) and cannot be refreshed.
/// In production, the Foundry platform injects a managed identity automatically.
///
/// Generate a token on your host and pass it to the container:
/// export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
/// docker run -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN ...
/// </summary>
internal sealed class DevTemporaryTokenCredential : TokenCredential
{
private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
private readonly string? _token;
public DevTemporaryTokenCredential()
{
this._token = Environment.GetEnvironmentVariable(EnvironmentVariable);
}
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return this.GetAccessToken();
}
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return new ValueTask<AccessToken>(this.GetAccessToken());
}
private AccessToken GetAccessToken()
{
if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential")
{
throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
}
return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1));
}
}
@@ -0,0 +1,121 @@
# Hosted-FoundryAgent
A hosted agent that delegates to a **Foundry-managed agent definition**. Instead of defining the model, instructions, and tools inline in code, this sample retrieves an existing agent registered in the Foundry platform via `AIProjectClient.AsAIAgent(agentRecord)` and hosts it using the Responses protocol.
This is the **Foundry hosting** pattern — the agent's behavior is configured in the platform (via Foundry UI, CLI, or API), and this server simply wraps and serves it.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- An Azure AI Foundry project with a **registered agent** (created via Foundry UI, CLI, or API)
- Azure CLI logged in (`az login`)
## Configuration
Copy the template and fill in your project endpoint:
```bash
cp .env.example .env
```
Edit `.env` and set your Azure AI Foundry project endpoint:
```env
AZURE_AI_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
```
> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
You also need to set `AGENT_NAME` — the name of the Foundry-managed agent to host. This is injected automatically by the Foundry platform when deployed. For local development, pass it as an environment variable.
## Running directly (contributors)
This project uses `ProjectReference` to build against the local Agent Framework source.
```bash
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent
AGENT_NAME=<your-agent-name> dotnet run
```
The agent will start on `http://localhost:8088`.
### Test it
Using the Azure Developer CLI:
```bash
azd ai agent invoke --local "Hello!"
```
Or with curl (specifying the agent name explicitly):
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "Hello!", "model": "<your-agent-name>"}'
```
## Running with Docker
Since this project uses `ProjectReference`, the standard `Dockerfile` cannot resolve dependencies outside this folder. Use `Dockerfile.contributor` which takes a pre-published output.
### 1. Publish for the container runtime (Linux Alpine)
```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
```
### 2. Build the Docker image
```bash
docker build -f Dockerfile.contributor -t hosted-foundry-agent .
```
### 3. Run the container
Generate a bearer token on your host and pass it to the container:
```bash
# Generate token (expires in ~1 hour)
export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
# Run with token
docker run --rm -p 8088:8088 \
-e AGENT_NAME=<your-agent-name> \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
--env-file .env \
hosted-foundry-agent
```
> **Note:** `AGENT_NAME` is passed via `-e` to simulate the platform injection. `AZURE_BEARER_TOKEN` provides Azure credentials to the container (tokens expire after ~1 hour). The `.env` file provides the remaining configuration.
### 4. Test it
Using the Azure Developer CLI:
```bash
azd ai agent invoke --local "Hello!"
```
Or with curl (specifying the agent name explicitly):
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "Hello!", "model": "<your-agent-name>"}'
```
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor` — it performs a full `dotnet restore` and `dotnet publish` inside the container. See the commented section in `HostedFoundryAgent.csproj` for the `PackageReference` alternative.
## How it differs from Hosted-ChatClientAgent
| | Hosted-ChatClientAgent | Hosted-FoundryAgent |
|---|---|---|
| **Agent definition** | Inline in code (`AsAIAgent(model, instructions)`) | Managed in Foundry platform (`AsAIAgent(agentRecord)`) |
| **Model/instructions** | Set in `Program.cs` | Set in Foundry UI/CLI/API |
| **Tools** | Defined in code | Configured in the platform |
| **Use case** | Full control over agent behavior | Platform-managed agent with centralized config |
@@ -0,0 +1,28 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-foundry-agent
displayName: "Hosted Foundry Agent"
description: >
A simple general-purpose AI assistant hosted as a Foundry Hosted Agent,
backed by a Foundry-managed agent definition.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Streaming
- Agent Framework
template:
name: hosted-foundry-agent
kind: hosted
protocols:
- protocol: responses
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
parameters:
properties: []
resources: []
@@ -0,0 +1,9 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-foundry-agent
protocols:
- protocol: responses
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
@@ -0,0 +1,5 @@
AZURE_AI_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
@@ -0,0 +1,17 @@
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedLocalTools.dll"]
@@ -0,0 +1,19 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
# which means a standard multi-stage Docker build cannot resolve dependencies outside
# this folder. Instead, pre-publish the app targeting the container runtime and copy
# the output into the container:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-local-tools .
# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-local-tools -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-local-tools
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedLocalTools.dll"]
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedLocalTools</RootNamespace>
<AssemblyName>HostedLocalTools</AssemblyName>
<NoWarn>$(NoWarn);</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" VersionOverride="2.1.0-beta.1" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
</ItemGroup>
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
</ItemGroup>
-->
</Project>
@@ -0,0 +1,164 @@
// Copyright (c) Microsoft. All rights reserved.
// Seattle Hotel Agent - A hosted agent with local C# function tools.
// Demonstrates how to define and wire local tools that the LLM can invoke,
// a key advantage of code-based hosted agents over prompt agents.
using System.ComponentModel;
using System.Globalization;
using System.Text;
using Azure.AI.Projects;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Extensions.AI;
// Load .env file if present (for local development)
Env.TraversePath().Load();
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());
// ── Hotel data ───────────────────────────────────────────────────────────────
Hotel[] seattleHotels =
[
new("Contoso Suites", 189, 4.5, "Downtown"),
new("Fabrikam Residences", 159, 4.2, "Pike Place Market"),
new("Alpine Ski House", 249, 4.7, "Seattle Center"),
new("Margie's Travel Lodge", 219, 4.4, "Waterfront"),
new("Northwind Inn", 139, 4.0, "Capitol Hill"),
new("Relecloud Hotel", 99, 3.8, "University District"),
];
// ── Tool: GetAvailableHotels ─────────────────────────────────────────────────
[Description("Get available hotels in Seattle for the specified dates.")]
string GetAvailableHotels(
[Description("Check-in date in YYYY-MM-DD format")] string checkInDate,
[Description("Check-out date in YYYY-MM-DD format")] string checkOutDate,
[Description("Maximum price per night in USD (optional, defaults to 500)")] int maxPrice = 500)
{
if (!DateTime.TryParseExact(checkInDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkIn))
{
return "Error parsing check-in date. Please use YYYY-MM-DD format.";
}
if (!DateTime.TryParseExact(checkOutDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkOut))
{
return "Error parsing check-out date. Please use YYYY-MM-DD format.";
}
if (checkOut <= checkIn)
{
return "Error: Check-out date must be after check-in date.";
}
int nights = (checkOut - checkIn).Days;
List<Hotel> availableHotels = seattleHotels.Where(h => h.PricePerNight <= maxPrice).ToList();
if (availableHotels.Count == 0)
{
return $"No hotels found in Seattle within your budget of ${maxPrice}/night.";
}
StringBuilder result = new();
result.AppendLine($"Available hotels in Seattle from {checkInDate} to {checkOutDate} ({nights} nights):");
result.AppendLine();
foreach (Hotel hotel in availableHotels)
{
int totalCost = hotel.PricePerNight * nights;
result.AppendLine($"**{hotel.Name}**");
result.AppendLine($" Location: {hotel.Location}");
result.AppendLine($" Rating: {hotel.Rating}/5");
result.AppendLine($" ${hotel.PricePerNight}/night (Total: ${totalCost})");
result.AppendLine();
}
return result.ToString();
}
// ── Create and host the agent ────────────────────────────────────────────────
AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
.AsAIAgent(
model: deploymentName,
instructions: """
You are a helpful travel assistant specializing in finding hotels in Seattle, Washington.
When a user asks about hotels in Seattle:
1. Ask for their check-in and check-out dates if not provided
2. Ask about their budget preferences if not mentioned
3. Use the GetAvailableHotels tool to find available options
4. Present the results in a friendly, informative way
5. Offer to help with additional questions about the hotels or Seattle
Be conversational and helpful. If users ask about things outside of Seattle hotels,
politely let them know you specialize in Seattle hotel recommendations.
""",
name: Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-local-tools",
description: "Seattle hotel search agent with local function tools",
tools: [AIFunctionFactory.Create(GetAvailableHotels)]);
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
var app = builder.Build();
app.MapFoundryResponses();
if (app.Environment.IsDevelopment())
{
app.MapFoundryResponses("openai/v1");
}
app.Run();
// ── Types ────────────────────────────────────────────────────────────────────
internal sealed record Hotel(string Name, int PricePerNight, double Rating, string Location);
/// <summary>
/// A <see cref="TokenCredential"/> for local Docker debugging only.
/// Reads a pre-fetched bearer token from the <c>AZURE_BEARER_TOKEN</c> environment variable
/// once at startup. This should NOT be used in production.
///
/// Generate a token on your host and pass it to the container:
/// export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
/// docker run -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN ...
/// </summary>
internal sealed class DevTemporaryTokenCredential : TokenCredential
{
private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
private readonly string? _token;
public DevTemporaryTokenCredential()
{
this._token = Environment.GetEnvironmentVariable(EnvironmentVariable);
}
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> this.GetAccessToken();
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> new(this.GetAccessToken());
private AccessToken GetAccessToken()
{
if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential")
{
throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
}
return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1));
}
}
@@ -0,0 +1,113 @@
# Hosted-LocalTools
A hosted agent with **local C# function tools** for hotel search. Demonstrates how to define and wire local tools that the LLM can invoke — a key advantage of code-based hosted agents over prompt agents.
The agent specializes in finding hotels in Seattle, with a `GetAvailableHotels` tool that searches a mock hotel database by dates and budget.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`)
- Azure CLI logged in (`az login`)
## Configuration
Copy the template and fill in your project endpoint:
```bash
cp .env.example .env
```
Edit `.env` and set your Azure AI Foundry project endpoint:
```env
AZURE_AI_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
```
> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
## Running directly (contributors)
This project uses `ProjectReference` to build against the local Agent Framework source.
```bash
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools
AGENT_NAME=hosted-local-tools dotnet run
```
The agent will start on `http://localhost:8088`.
### Test it
Using the Azure Developer CLI:
```bash
azd ai agent invoke --local "Find me a hotel in Seattle for Dec 20-25 under $200/night"
```
Or with curl:
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "Find me a hotel in Seattle for Dec 20-25 under $200/night", "model": "hosted-local-tools"}'
```
## Running with Docker
Since this project uses `ProjectReference`, use `Dockerfile.contributor` which takes a pre-published output.
### 1. Publish for the container runtime (Linux Alpine)
```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
```
### 2. Build the Docker image
```bash
docker build -f Dockerfile.contributor -t hosted-local-tools .
```
### 3. Run the container
Generate a bearer token on your host and pass it to the container:
```bash
# Generate token (expires in ~1 hour)
export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
# Run with token
docker run --rm -p 8088:8088 \
-e AGENT_NAME=hosted-local-tools \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
--env-file .env \
hosted-local-tools
```
### 4. Test it
Using the Azure Developer CLI:
```bash
azd ai agent invoke --local "What hotels are available in Seattle for next weekend?"
```
## How local tools work
The agent has a single tool `GetAvailableHotels` defined as a C# method with `[Description]` attributes. The LLM decides when to call it based on the user's request:
| Parameter | Type | Description |
|-----------|------|-------------|
| `checkInDate` | string | Check-in date (YYYY-MM-DD) |
| `checkOutDate` | string | Check-out date (YYYY-MM-DD) |
| `maxPrice` | int | Max price per night in USD (default: 500) |
The tool searches a mock database of 6 Seattle hotels and returns formatted results with name, location, rating, and pricing.
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedLocalTools.csproj` for the `PackageReference` alternative.
@@ -0,0 +1,29 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-local-tools
displayName: "Seattle Hotel Agent with Local Tools"
description: >
A travel assistant agent that helps users find hotels in Seattle.
Demonstrates local C# tool execution — a key advantage of code-based
hosted agents over prompt agents.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Local Tools
- Agent Framework
template:
name: hosted-local-tools
kind: hosted
protocols:
- protocol: responses
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
parameters:
properties: []
resources: []
@@ -0,0 +1,9 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-local-tools
protocols:
- protocol: responses
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
@@ -0,0 +1,5 @@
AZURE_AI_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
@@ -0,0 +1,17 @@
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedMcpTools.dll"]
@@ -0,0 +1,18 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local source, which means a standard
# multi-stage Docker build cannot resolve dependencies outside this folder.
# Pre-publish the app targeting the container runtime and copy the output:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-mcp-tools .
# docker run --rm -p 8088:8088 -e AGENT_NAME=mcp-tools -e GITHUB_PAT=$GITHUB_PAT -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-mcp-tools
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedMcpTools.dll"]
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedMcpTools</RootNamespace>
<AssemblyName>HostedMcpTools</AssemblyName>
<NoWarn>$(NoWarn);</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" VersionOverride="2.1.0-beta.1" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="ModelContextProtocol" VersionOverride="1.2.0" />
<PackageReference Include="DotNetEnv" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
</ItemGroup>
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
</ItemGroup>
-->
</Project>
@@ -0,0 +1,130 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates a hosted agent with two layers of MCP (Model Context Protocol) tools:
//
// 1. CLIENT-SIDE MCP: The agent connects to the Microsoft Learn MCP server directly via
// McpClient, discovers tools, and handles tool invocations locally within the agent process.
//
// 2. SERVER-SIDE MCP: The agent declares a HostedMcpServerTool for the same MCP server which
// delegates tool discovery and invocation to the LLM provider (Azure OpenAI Responses API).
// The provider calls the MCP server on behalf of the agent — no local connection needed.
//
// Both patterns use the Microsoft Learn MCP server to illustrate the architectural difference:
// client-side tools are resolved and invoked by the agent, while server-side tools are resolved
// and invoked by the LLM provider.
#pragma warning disable MEAI001 // HostedMcpServerTool is experimental
using Azure.AI.Projects;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
// Load .env file if present (for local development)
Env.TraversePath().Load();
var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."));
var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());
// ── Client-side MCP: Microsoft Learn (local resolution) ──────────────────────
// Connect directly to the MCP server. The agent discovers and invokes tools locally.
Console.WriteLine("Connecting to Microsoft Learn MCP server (client-side)...");
await using var learnMcp = await McpClient.CreateAsync(new HttpClientTransport(new()
{
Endpoint = new Uri("https://learn.microsoft.com/api/mcp"),
Name = "Microsoft Learn (client)",
}));
var clientTools = await learnMcp.ListToolsAsync();
Console.WriteLine($"Client-side MCP tools: {string.Join(", ", clientTools.Select(t => t.Name))}");
// ── Server-side MCP: Microsoft Learn (provider resolution) ───────────────────
// Declare a HostedMcpServerTool — the LLM provider (Responses API) handles tool
// invocations directly. No local MCP connection needed for this pattern.
AITool serverTool = new HostedMcpServerTool(
serverName: "microsoft_learn_hosted",
serverAddress: "https://learn.microsoft.com/api/mcp")
{
AllowedTools = ["microsoft_docs_search"],
ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire
};
Console.WriteLine("Server-side MCP tool: microsoft_docs_search (via HostedMcpServerTool)");
// ── Combine both tool types into a single agent ──────────────────────────────
// The agent has access to tools from both MCP patterns simultaneously.
List<AITool> allTools = [.. clientTools.Cast<AITool>(), serverTool];
AIAgent agent = new AIProjectClient(projectEndpoint, credential)
.AsAIAgent(
model: deployment,
instructions: """
You are a helpful developer assistant with access to Microsoft Learn documentation.
Use the available tools to search and retrieve documentation.
Be concise and provide direct answers with relevant links.
""",
name: "mcp-tools",
description: "Developer assistant with dual-layer MCP tools (client-side and server-side)",
tools: allTools);
// Host the agent as a Foundry Hosted Agent using the Responses API.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
var app = builder.Build();
app.MapFoundryResponses();
// In Development, also map the OpenAI-compatible route that AIProjectClient uses.
if (app.Environment.IsDevelopment())
{
app.MapFoundryResponses("openai/v1");
}
app.Run();
/// <summary>
/// A <see cref="TokenCredential"/> for local Docker debugging only.
/// Reads a pre-fetched bearer token from the <c>AZURE_BEARER_TOKEN</c> environment variable
/// once at startup. This should NOT be used in production.
///
/// Generate a token on your host and pass it to the container:
/// export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
/// docker run -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN ...
/// </summary>
internal sealed class DevTemporaryTokenCredential : TokenCredential
{
private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
private readonly string? _token;
public DevTemporaryTokenCredential()
{
this._token = Environment.GetEnvironmentVariable(EnvironmentVariable);
}
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> this.GetAccessToken();
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> new(this.GetAccessToken());
private AccessToken GetAccessToken()
{
if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential")
{
throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
}
return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1));
}
}
@@ -0,0 +1,83 @@
# Hosted-McpTools
A hosted agent demonstrating **two layers of MCP (Model Context Protocol) tool integration**:
1. **Client-side MCP (Microsoft Learn)** — The agent connects directly to the Microsoft Learn MCP server via `McpClient`, discovers tools, and handles tool invocations locally within the agent process.
2. **Server-side MCP (Microsoft Learn)** — The agent declares a `HostedMcpServerTool` which delegates tool discovery and invocation to the LLM provider (Azure OpenAI Responses API). The provider calls the MCP server on behalf of the agent with no local connection needed.
## How the two MCP patterns differ
| | Client-side MCP | Server-side MCP |
|---|---|---|
| **Connection** | Agent connects to MCP server directly | LLM provider connects to MCP server |
| **Tool invocation** | Handled by the agent process | Handled by the Responses API |
| **Auth** | Agent manages credentials | Provider manages credentials |
| **Use case** | Custom/private MCP servers, fine-grained control | Public MCP servers, simpler setup |
| **Example** | Microsoft Learn (`McpClient` + `HttpClientTransport`) | Microsoft Learn (`HostedMcpServerTool`) |
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`)
- Azure CLI logged in (`az login`)
## Configuration
Copy the template and fill in your values:
```bash
cp .env.example .env
```
Edit `.env`:
```env
AZURE_AI_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
```
## Running directly (contributors)
```bash
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools
dotnet run
```
### Test it
Using the Azure Developer CLI:
```bash
# Uses GitHub MCP (client-side)
azd ai agent invoke --local "Search for the agent-framework repository on GitHub"
# Uses Microsoft Learn MCP (server-side)
azd ai agent invoke --local "How do I create an Azure storage account using az cli?"
```
## Running with Docker
### 1. Publish for the container runtime
```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
```
### 2. Build and run
```bash
docker build -f Dockerfile.contributor -t hosted-mcp-tools .
export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
docker run --rm -p 8088:8088 \
-e AGENT_NAME=mcp-tools \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
--env-file .env \
hosted-mcp-tools
```
## NuGet package users
Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedMcpTools.csproj` for the `PackageReference` alternative.
@@ -0,0 +1,30 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: mcp-tools
displayName: "MCP Tools Agent"
description: >
A developer assistant demonstrating dual-layer MCP integration:
client-side GitHub MCP tools handled by the agent and server-side
Microsoft Learn MCP tools delegated to the LLM provider.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Agent Framework
- MCP
- Model Context Protocol
template:
name: mcp-tools
kind: hosted
protocols:
- protocol: responses
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
parameters:
properties: []
resources: []
@@ -0,0 +1,9 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: mcp-tools
protocols:
- protocol: responses
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
@@ -0,0 +1,5 @@
AZURE_AI_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
@@ -0,0 +1,17 @@
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedTextRag.dll"]
@@ -0,0 +1,19 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
# which means a standard multi-stage Docker build cannot resolve dependencies outside
# this folder. Instead, pre-publish the app targeting the container runtime and copy
# the output into the container:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-text-rag .
# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-text-rag -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-text-rag
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedTextRag.dll"]
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedTextRag</RootNamespace>
<AssemblyName>HostedTextRag</AssemblyName>
<NoWarn>$(NoWarn);</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" VersionOverride="2.1.0-beta.1" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0" />
</ItemGroup>
-->
</Project>
@@ -0,0 +1,130 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to use TextSearchProvider to add retrieval augmented generation (RAG)
// capabilities to a hosted agent. The provider runs a search against an external knowledge base
// before each model invocation and injects the results into the model context.
using Azure.AI.Projects;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
// Load .env file if present (for local development)
Env.TraversePath().Load();
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());
TextSearchProviderOptions textSearchOptions = new()
{
SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke,
RecentMessageMemoryLimit = 6,
};
AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
.AsAIAgent(new ChatClientAgentOptions
{
Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-text-rag",
ChatOptions = new ChatOptions
{
ModelId = deploymentName,
Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
},
AIContextProviders = [new TextSearchProvider(MockSearchAsync, textSearchOptions)]
});
// Host the agent as a Foundry Hosted Agent using the Responses API.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
var app = builder.Build();
app.MapFoundryResponses();
if (app.Environment.IsDevelopment())
{
app.MapFoundryResponses("openai/v1");
}
app.Run();
// ── Mock search function ─────────────────────────────────────────────────────
// In production, replace this with a real search provider (e.g., Azure AI Search).
static Task<IEnumerable<TextSearchProvider.TextSearchResult>> MockSearchAsync(string query, CancellationToken cancellationToken)
{
List<TextSearchProvider.TextSearchResult> results = [];
if (query.Contains("return", StringComparison.OrdinalIgnoreCase) || query.Contains("refund", StringComparison.OrdinalIgnoreCase))
{
results.Add(new()
{
SourceName = "Contoso Outdoors Return Policy",
SourceLink = "https://contoso.com/policies/returns",
Text = "Customers may return any item within 30 days of delivery. Items should be unused and include original packaging. Refunds are issued to the original payment method within 5 business days of inspection."
});
}
if (query.Contains("shipping", StringComparison.OrdinalIgnoreCase))
{
results.Add(new()
{
SourceName = "Contoso Outdoors Shipping Guide",
SourceLink = "https://contoso.com/help/shipping",
Text = "Standard shipping is free on orders over $50 and typically arrives in 3-5 business days within the continental United States. Expedited options are available at checkout."
});
}
if (query.Contains("tent", StringComparison.OrdinalIgnoreCase) || query.Contains("fabric", StringComparison.OrdinalIgnoreCase))
{
results.Add(new()
{
SourceName = "TrailRunner Tent Care Instructions",
SourceLink = "https://contoso.com/manuals/trailrunner-tent",
Text = "Clean the tent fabric with lukewarm water and a non-detergent soap. Allow it to air dry completely before storage and avoid prolonged UV exposure to extend the lifespan of the waterproof coating."
});
}
return Task.FromResult<IEnumerable<TextSearchProvider.TextSearchResult>>(results);
}
/// <summary>
/// A <see cref="TokenCredential"/> for local Docker debugging only.
/// Reads a pre-fetched bearer token from the <c>AZURE_BEARER_TOKEN</c> environment variable.
/// This should NOT be used in production — tokens expire (~1 hour) and cannot be refreshed.
///
/// Generate a token on your host and pass it to the container:
/// export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
/// docker run -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN ...
/// </summary>
internal sealed class DevTemporaryTokenCredential : TokenCredential
{
private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> GetAccessToken();
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> new(GetAccessToken());
private static AccessToken GetAccessToken()
{
var token = Environment.GetEnvironmentVariable(EnvironmentVariable);
if (string.IsNullOrEmpty(token) || token == "DefaultAzureCredential")
{
throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
}
return new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1));
}
}
@@ -0,0 +1,116 @@
# Hosted-TextRag
A hosted agent with **Retrieval Augmented Generation (RAG)** capabilities using `TextSearchProvider`. The agent grounds its answers in product documentation by running a search before each model invocation, then citing the source in its response.
This sample demonstrates how to add knowledge grounding to a hosted agent without requiring an external search index — using a mock search function that can be replaced with Azure AI Search or any other provider.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`)
- Azure CLI logged in (`az login`)
## Configuration
Copy the template and fill in your project endpoint:
```bash
cp .env.example .env
```
Edit `.env` and set your Azure AI Foundry project endpoint:
```env
AZURE_AI_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
AZURE_BEARER_TOKEN=
```
> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
## Running directly (contributors)
This project uses `ProjectReference` to build against the local Agent Framework source.
```bash
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag
AGENT_NAME=hosted-text-rag dotnet run
```
The agent will start on `http://localhost:8088`.
### Test it
Using the Azure Developer CLI:
```bash
azd ai agent invoke --local "What is your return policy?"
azd ai agent invoke --local "How long does shipping take?"
azd ai agent invoke --local "How do I clean my tent?"
```
Or with curl:
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "What is your return policy?", "model": "hosted-text-rag"}'
```
## Running with Docker
Since this project uses `ProjectReference`, use `Dockerfile.contributor` which takes a pre-published output.
### 1. Publish for the container runtime (Linux Alpine)
```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
```
### 2. Build the Docker image
```bash
docker build -f Dockerfile.contributor -t hosted-text-rag .
```
### 3. Run the container
Generate a bearer token on your host and pass it to the container:
```bash
# Generate token (expires in ~1 hour)
export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
# Run with token
docker run --rm -p 8088:8088 \
-e AGENT_NAME=hosted-text-rag \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
--env-file .env \
hosted-text-rag
```
### 4. Test it
Using the Azure Developer CLI:
```bash
azd ai agent invoke --local "What is your return policy?"
```
## How RAG works in this sample
The `TextSearchProvider` runs a mock search **before each model invocation**:
| User query contains | Search result injected |
|---|---|
| "return" or "refund" | Contoso Outdoors Return Policy |
| "shipping" | Contoso Outdoors Shipping Guide |
| "tent" or "fabric" | TrailRunner Tent Care Instructions |
The model receives the search results as additional context and cites the source in its response. In production, replace `MockSearchAsync` with a call to Azure AI Search or your preferred search provider.
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedTextRag.csproj` for the `PackageReference` alternative.
@@ -0,0 +1,30 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-text-rag
displayName: "Hosted Text RAG Agent"
description: >
A support specialist agent for Contoso Outdoors with RAG capabilities.
Uses TextSearchProvider to ground answers in product documentation
before each model invocation.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- RAG
- Text Search
- Agent Framework
template:
name: hosted-text-rag
kind: hosted
protocols:
- protocol: responses
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
parameters:
properties: []
resources: []
@@ -0,0 +1,9 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-text-rag
protocols:
- protocol: responses
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedToolbox</RootNamespace>
<AssemblyName>HostedToolbox</AssemblyName>
<NoWarn>$(NoWarn);</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" VersionOverride="2.1.0-beta.1" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
</ItemGroup>
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
</ItemGroup>
-->
</Project>
@@ -0,0 +1,113 @@
// Copyright (c) Microsoft. All rights reserved.
// Foundry Toolbox Agent - A hosted agent that uses Foundry Toolset MCP tools.
//
// Demonstrates how to register one or more Foundry toolsets so the agent can
// call tools provided by the Foundry platform's managed MCP proxy.
//
// Required environment variables:
// AZURE_AI_PROJECT_ENDPOINT - Azure AI Foundry project endpoint
// AZURE_AI_MODEL_DEPLOYMENT_NAME - Model deployment name (default: gpt-4o)
// FOUNDRY_AGENT_TOOLSET_ENDPOINT - Foundry Toolsets proxy base URL
// (injected automatically by Foundry platform at runtime)
//
// Optional:
// FOUNDRY_TOOLBOX_NAME - Name of the toolset to load (default: my-toolset)
// FOUNDRY_AGENT_NAME - Client name reported to MCP server
// FOUNDRY_AGENT_VERSION - Client version reported to MCP server
// FOUNDRY_AGENT_TOOLSET_FEATURES - Feature flags sent to Foundry proxy via header
using Azure.AI.Projects;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
// Load .env file if present (for local development)
Env.TraversePath().Load();
string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
string toolboxName = Environment.GetEnvironmentVariable("FOUNDRY_TOOLBOX_NAME") ?? "my-toolset";
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());
// ── Create agent ─────────────────────────────────────────────────────────────
AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
.AsAIAgent(
model: deploymentName,
instructions: """
You are a helpful assistant with access to tools provided by the Foundry Toolset.
Use the available tools to answer user questions.
If a tool is not available for a request, let the user know clearly.
""",
name: Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-toolbox-agent",
description: "Hosted agent backed by Foundry Toolset MCP tools");
// ── Build the host ────────────────────────────────────────────────────────────
var builder = WebApplication.CreateBuilder(args);
// Register the agent and response handler
builder.Services.AddFoundryResponses(agent);
// Register Foundry Toolbox: connects to the MCP proxy at startup and makes tools available.
// The toolset name must match a toolset registered in your Foundry project.
// When FOUNDRY_AGENT_TOOLSET_ENDPOINT is absent (e.g., in local development without Foundry
// infrastructure), startup succeeds without error and no toolbox tools are loaded.
builder.Services.AddFoundryToolboxes(toolboxName);
var app = builder.Build();
app.MapFoundryResponses();
if (app.Environment.IsDevelopment())
{
app.MapFoundryResponses("openai/v1");
}
app.Run();
// ── DevTemporaryTokenCredential ───────────────────────────────────────────────
/// <summary>
/// A <see cref="TokenCredential"/> for local Docker debugging only.
/// Reads a pre-fetched bearer token from the <c>AZURE_BEARER_TOKEN</c> environment variable
/// once at startup. This should NOT be used in production.
///
/// Generate a token on your host and pass it to the container:
/// export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
/// docker run -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN ...
/// </summary>
internal sealed class DevTemporaryTokenCredential : TokenCredential
{
private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
private readonly string? _token;
public DevTemporaryTokenCredential()
{
this._token = Environment.GetEnvironmentVariable(EnvironmentVariable);
}
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> this.GetAccessToken();
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> new(this.GetAccessToken());
private AccessToken GetAccessToken()
{
if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential")
{
throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
}
return new AccessToken(this._token, DateTimeOffset.MaxValue);
}
}
@@ -0,0 +1,5 @@
AZURE_OPENAI_ENDPOINT=https://<your-account>.openai.azure.com/
AZURE_OPENAI_DEPLOYMENT=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
@@ -0,0 +1,17 @@
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedWorkflowHandoff.dll"]
@@ -0,0 +1,19 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
# which means a standard multi-stage Docker build cannot resolve dependencies outside
# this folder. Instead, pre-publish the app targeting the container runtime and copy
# the output into the container:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-workflow-handoff .
# docker run --rm -p 8088:8088 -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-workflow-handoff
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedWorkflowHandoff.dll"]
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>HostedWorkflowHandoff</RootNamespace>
<AssemblyName>HostedWorkflowHandoff</AssemblyName>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<NoWarn>$(NoWarn);NU1605;MAAIW001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Core" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="ModelContextProtocol" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
</ItemGroup>
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" />
<PackageReference Include="Microsoft.Agents.AI.Hosting" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
<PackageReference Include="Microsoft.Agents.AI.Workflows" />
</ItemGroup>
-->
</Project>
@@ -0,0 +1,470 @@
// Copyright (c) Microsoft. All rights reserved.
/// <summary>
/// Static HTML pages served by the sample application.
/// </summary>
internal static class Pages
{
// ═══════════════════════════════════════════════════════════════════════
// Homepage
// ═══════════════════════════════════════════════════════════════════════
internal const string Home = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Foundry Responses Hosting Demos</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f5f5f5; display: flex; justify-content: center; padding: 2rem; }
main { width: 100%; max-width: 700px; }
h1 { font-size: 1.5rem; margin-bottom: .5rem; color: #1a1a1a; }
.subtitle { color: #555; margin-bottom: 2rem; line-height: 1.5; }
.cards { display: flex; flex-direction: column; gap: 1rem; }
.card { background: #fff; border: 1px solid #ddd; border-radius: 10px; padding: 1.5rem; text-decoration: none; color: inherit; transition: box-shadow .15s, transform .15s; }
.card:hover { box-shadow: 0 4px 16px rgba(0,0,0,.1); transform: translateY(-2px); }
.card h2 { font-size: 1.15rem; color: #0066cc; margin-bottom: .4rem; }
.card p { color: #555; line-height: 1.5; font-size: .9rem; }
.card .tags { margin-top: .6rem; display: flex; gap: .4rem; flex-wrap: wrap; }
.card .tag { background: #e8f0fe; color: #1a73e8; padding: .15rem .5rem; border-radius: 12px; font-size: .75rem; }
footer { margin-top: 2rem; font-size: .8rem; color: #999; text-align: center; }
</style>
</head>
<body>
<main>
<h1>🚀 Foundry Responses Hosting</h1>
<p class="subtitle">
Agent-framework agents hosted via the Azure AI Responses Server SDK.<br/>
Each demo registers a different agent and serves it through <code>POST /responses</code>.
</p>
<div class="cards">
<a class="card" href="/tool-demo">
<h2>🔧 Tool Demo</h2>
<p>An agent with local function tools (time, weather) and remote MCP tools from
Microsoft Learn for documentation search.</p>
<div class="tags">
<span class="tag">Local Tools</span>
<span class="tag">MCP</span>
<span class="tag">Microsoft Learn</span>
<span class="tag">Streaming</span>
</div>
</a>
<a class="card" href="/workflow-demo">
<h2>🔀 Workflow Demo</h2>
<p>A triage workflow that routes questions to specialist agents a Code Expert
or a Creative Writer using agent handoffs.</p>
<div class="tags">
<span class="tag">Workflow</span>
<span class="tag">Handoffs</span>
<span class="tag">Multi-Agent</span>
<span class="tag">Triage</span>
</div>
</a>
</div>
<footer>
All demos share the same <code>/responses</code> endpoint.
The <code>model</code> field in the request selects which agent handles it.
</footer>
</main>
</body>
</html>
""";
// ═══════════════════════════════════════════════════════════════════════
// Tool Demo
// ═══════════════════════════════════════════════════════════════════════
internal const string ToolDemo = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Tool Demo Foundry Responses Hosting</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f5f5f5; display: flex; justify-content: center; padding: 2rem; }
main { width: 100%; max-width: 800px; }
h1 { font-size: 1.2rem; margin-bottom: .3rem; color: #333; }
.subtitle { font-size: .85rem; color: #666; margin-bottom: .8rem; }
a.back { font-size: .85rem; color: #0066cc; text-decoration: none; display: inline-block; margin-bottom: 1rem; }
#chat { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 1rem; height: 56vh; overflow-y: auto; margin-bottom: 1rem; }
.msg { margin-bottom: .75rem; line-height: 1.6; }
.msg.user { color: #0066cc; }
.msg.assistant { color: #333; }
.msg .role { font-weight: 600; margin-right: .25rem; }
.tool-call { background: #f0f4ff; border-left: 3px solid #4a90d9; padding: .4rem .6rem; margin: .4rem 0; border-radius: 4px; font-size: .85rem; color: #555; font-family: 'Cascadia Code', 'Fira Code', monospace; }
.tool-call .tool-icon { margin-right: .3rem; }
form { display: flex; gap: .5rem; }
input { flex: 1; padding: .6rem .8rem; border: 1px solid #ccc; border-radius: 6px; font-size: 1rem; }
button { padding: .6rem 1.2rem; background: #0066cc; color: #fff; border: none; border-radius: 6px; font-size: 1rem; cursor: pointer; }
button:disabled { opacity: .5; cursor: not-allowed; }
#status { font-size: .85rem; color: #888; margin-top: .5rem; }
.suggestions { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: 1rem; }
.suggestions button { padding: .3rem .7rem; font-size: .8rem; background: #e8f0fe; color: #1a73e8; border: 1px solid #c5d8f8; border-radius: 16px; cursor: pointer; }
.suggestions button:hover { background: #d2e3fc; }
</style>
</head>
<body>
<main>
<a class="back" href="/"> Back to demos</a>
<h1>🔧 Tool Demo</h1>
<p class="subtitle">Agent with local tools (time, weather) + Microsoft Learn MCP (docs search)</p>
<div class="suggestions">
<button onclick="sendText('What time is it in Tokyo?')">🕐 Time in Tokyo</button>
<button onclick="sendText('What is the weather in Seattle?')">🌤 Weather in Seattle</button>
<button onclick="sendText('How do I create an Azure Function using the CLI?')">📚 Azure Functions docs</button>
<button onclick="sendText('What is Microsoft Agent Framework?')">📚 Agent Framework</button>
</div>
<div id="chat"></div>
<form id="form">
<input id="input" placeholder="Try: 'What time is it?' or 'Search docs for Azure AI Foundry'" autocomplete="off" autofocus />
<button type="submit">Send</button>
</form>
<div id="status"></div>
</main>
<script src="/js/sse-validator.js"></script>
<script>
const AGENT = 'tool-agent';
const chat = document.getElementById('chat');
const form = document.getElementById('form');
const input = document.getElementById('input');
const status = document.getElementById('status');
function escapeHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function addMsg(role, html) {
const d = document.createElement('div');
d.className = 'msg ' + role; d.innerHTML = html;
chat.appendChild(d); chat.scrollTop = chat.scrollHeight; return d;
}
function addToolCall(name) {
const d = document.createElement('div');
d.className = 'tool-call';
d.innerHTML = '<span class="tool-icon">🔧</span> Calling <b>' + escapeHtml(name) + '</b>';
chat.appendChild(d); chat.scrollTop = chat.scrollHeight; return d;
}
function sendText(t) { input.value = t; form.dispatchEvent(new Event('submit')); }
form.addEventListener('submit', async e => {
e.preventDefault();
const text = input.value.trim(); if (!text) return;
input.value = '';
addMsg('user', '<span class="role">You:</span>' + escapeHtml(text));
const btn = form.querySelector('button[type="submit"]');
btn.disabled = true; status.textContent = 'Streaming';
let fullText = '', assistantDiv = null;
const toolCalls = {};
const validator = new SseValidator();
try {
const resp = await fetch('/responses', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: AGENT, stream: true, input: text })
});
if (!resp.ok) { status.textContent = 'Error ' + resp.status; btn.disabled = false; return; }
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buf = '', curEvt = null;
while (true) {
const { done, value } = await reader.read(); if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split('\n'); buf = lines.pop();
for (const line of lines) {
if (line.startsWith('event: ')) { curEvt = line.slice(7).trim(); continue; }
if (!line.startsWith('data: ')) continue;
const d = line.slice(6).trim(); if (d === '[DONE]') continue;
try {
const evt = JSON.parse(d);
validator.capture(curEvt || evt.type || 'unknown', d);
curEvt = null;
if (evt.type === 'response.output_item.added' && evt.item?.type === 'function_call') {
const id = evt.item.id;
toolCalls[id] = { name: evt.item.name || '?', args: '', el: addToolCall(evt.item.name || '?') };
status.textContent = 'Calling tool: ' + (evt.item.name || '…');
}
if (evt.type === 'response.function_call_arguments.delta' && evt.item_id && toolCalls[evt.item_id])
toolCalls[evt.item_id].args += (evt.delta || '');
if (evt.type === 'response.function_call_arguments.done' && evt.item_id && toolCalls[evt.item_id]) {
const tc = toolCalls[evt.item_id];
let args = tc.args; try { args = JSON.stringify(JSON.parse(args), null, 0); } catch {}
tc.el.innerHTML = '<span class="tool-icon"></span> Called <b>' + escapeHtml(tc.name) + '</b>(' + escapeHtml(args) + ')';
}
if (evt.type === 'response.output_text.delta') {
if (!assistantDiv) assistantDiv = addMsg('assistant', '<span class="role">Agent:</span>');
fullText += evt.delta;
assistantDiv.innerHTML = '<span class="role">Agent:</span>' + escapeHtml(fullText);
chat.scrollTop = chat.scrollHeight;
status.textContent = 'Streaming';
}
} catch {}
}
}
if (!fullText && !assistantDiv) addMsg('assistant', '<span class="role">Agent:</span><em>(empty)</em>');
status.textContent = '';
} catch (err) { status.textContent = 'Error: ' + err.message; }
if (validator.events.length > 0) {
try { const vr = await validator.validate(); chat.appendChild(validator.renderElement(vr)); chat.scrollTop = chat.scrollHeight; } catch {}
}
btn.disabled = false; input.focus();
});
</script>
</body>
</html>
""";
// ═══════════════════════════════════════════════════════════════════════
// Workflow Demo
// ═══════════════════════════════════════════════════════════════════════
internal const string WorkflowDemo = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Workflow Demo Foundry Responses Hosting</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f5f5f5; display: flex; justify-content: center; padding: 2rem; }
main { width: 100%; max-width: 800px; }
h1 { font-size: 1.2rem; margin-bottom: .3rem; color: #333; }
.subtitle { font-size: .85rem; color: #666; margin-bottom: .8rem; }
a.back { font-size: .85rem; color: #0066cc; text-decoration: none; display: inline-block; margin-bottom: 1rem; }
#chat { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 1rem; height: 56vh; overflow-y: auto; margin-bottom: 1rem; }
.msg { margin-bottom: .75rem; line-height: 1.6; }
.msg.user { color: #0066cc; }
.msg.assistant { color: #333; }
.msg .role { font-weight: 600; margin-right: .25rem; }
.workflow-evt { background: #f0f9f0; border-left: 3px solid #4caf50; padding: .4rem .6rem; margin: .4rem 0; border-radius: 4px; font-size: .85rem; color: #555; }
.workflow-evt.failed { background: #fef0f0; border-left-color: #e53935; }
.tool-call { background: #f0f4ff; border-left: 3px solid #4a90d9; padding: .4rem .6rem; margin: .4rem 0; border-radius: 4px; font-size: .85rem; color: #555; font-family: 'Cascadia Code', 'Fira Code', monospace; }
form { display: flex; gap: .5rem; }
input { flex: 1; padding: .6rem .8rem; border: 1px solid #ccc; border-radius: 6px; font-size: 1rem; }
button { padding: .6rem 1.2rem; background: #0066cc; color: #fff; border: none; border-radius: 6px; font-size: 1rem; cursor: pointer; }
button:disabled { opacity: .5; cursor: not-allowed; }
#status { font-size: .85rem; color: #888; margin-top: .5rem; }
.suggestions { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: 1rem; }
.suggestions button { padding: .3rem .7rem; font-size: .8rem; background: #e8f0fe; color: #1a73e8; border: 1px solid #c5d8f8; border-radius: 16px; cursor: pointer; }
.suggestions button:hover { background: #d2e3fc; }
.agent-diagram { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin-bottom: 1rem; font-size: .85rem; text-align: center; color: #555; }
.agent-diagram .flow { font-size: 1.1rem; letter-spacing: 2px; }
</style>
</head>
<body>
<main>
<a class="back" href="/"> Back to demos</a>
<h1>🔀 Workflow Demo Agent Handoffs</h1>
<p class="subtitle">A triage agent routes your question to a specialist (Code Expert or Creative Writer)</p>
<div class="agent-diagram">
<div class="flow">👤 User 🔀 <b>Triage</b> 💻 <b>Code Expert</b> / <b>Creative Writer</b></div>
</div>
<div class="suggestions">
<button onclick="sendText('Write a Python function to reverse a linked list')">💻 Reverse linked list</button>
<button onclick="sendText('Write me a haiku about cloud computing')"> Cloud haiku</button>
<button onclick="sendText('Explain the difference between async and threads in C#')">💻 Async vs threads</button>
<button onclick="sendText('Write a short story about an AI that learns to paint')"> AI painter story</button>
</div>
<div id="chat"></div>
<form id="form">
<input id="input" placeholder="Ask a coding question or request creative writing…" autocomplete="off" autofocus />
<button type="submit">Send</button>
</form>
<div id="status"></div>
</main>
<script src="/js/sse-validator.js"></script>
<script>
const AGENT = 'triage-workflow';
const chat = document.getElementById('chat');
const form = document.getElementById('form');
const input = document.getElementById('input');
const status = document.getElementById('status');
function escapeHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function addMsg(role, html) {
const d = document.createElement('div');
d.className = 'msg ' + role; d.innerHTML = html;
chat.appendChild(d); chat.scrollTop = chat.scrollHeight; return d;
}
function addWorkflowEvent(icon, text, failed) {
const d = document.createElement('div');
d.className = 'workflow-evt' + (failed ? ' failed' : '');
d.innerHTML = icon + ' ' + escapeHtml(text);
chat.appendChild(d); chat.scrollTop = chat.scrollHeight;
}
function addToolCall(name) {
const d = document.createElement('div');
d.className = 'tool-call';
d.innerHTML = '🔀 Handoff: <b>' + escapeHtml(name) + '</b>';
chat.appendChild(d); chat.scrollTop = chat.scrollHeight; return d;
}
function sendText(t) { input.value = t; form.dispatchEvent(new Event('submit')); }
form.addEventListener('submit', async e => {
e.preventDefault();
const text = input.value.trim(); if (!text) return;
input.value = '';
addMsg('user', '<span class="role">You:</span>' + escapeHtml(text));
const btn = form.querySelector('button[type="submit"]');
btn.disabled = true; status.textContent = 'Running workflow';
let fullText = '', assistantDiv = null;
const toolCalls = {};
const validator = new SseValidator();
try {
const resp = await fetch('/responses', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: AGENT, stream: true, input: text })
});
if (!resp.ok) { status.textContent = 'Error ' + resp.status; btn.disabled = false; return; }
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buf = '', curEvt = null;
while (true) {
const { done, value } = await reader.read(); if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split('\n'); buf = lines.pop();
for (const line of lines) {
if (line.startsWith('event: ')) { curEvt = line.slice(7).trim(); continue; }
if (!line.startsWith('data: ')) continue;
const d = line.slice(6).trim(); if (d === '[DONE]') continue;
try {
const evt = JSON.parse(d);
validator.capture(curEvt || evt.type || 'unknown', d);
curEvt = null;
// Workflow events (executor invoked/completed/failed)
if (evt.type === 'response.output_item.added' && evt.item?.type === 'workflow_action') {
const s = evt.item.status;
const id = evt.item.action_id || evt.item.actionId || '?';
if (s === 'in_progress' || s === 'InProgress')
addWorkflowEvent('', 'Agent invoked: ' + id);
else if (s === 'completed' || s === 'Completed')
addWorkflowEvent('✅', 'Agent completed: ' + id);
else if (s === 'failed' || s === 'Failed')
addWorkflowEvent('❌', 'Agent failed: ' + id, true);
}
// Handoff function calls
if (evt.type === 'response.output_item.added' && evt.item?.type === 'function_call') {
const id = evt.item.id;
toolCalls[id] = { name: evt.item.name || '?', args: '', el: addToolCall(evt.item.name || '?') };
status.textContent = 'Handoff: ' + (evt.item.name || '…');
}
if (evt.type === 'response.function_call_arguments.delta' && evt.item_id && toolCalls[evt.item_id])
toolCalls[evt.item_id].args += (evt.delta || '');
if (evt.type === 'response.function_call_arguments.done' && evt.item_id && toolCalls[evt.item_id]) {
const tc = toolCalls[evt.item_id];
let args = tc.args; try { args = JSON.stringify(JSON.parse(args), null, 0); } catch {}
tc.el.innerHTML = '🔀 Handoff: <b>' + escapeHtml(tc.name) + '</b>(' + escapeHtml(args) + ')';
}
// Text streaming from the specialist agent
if (evt.type === 'response.output_text.delta') {
if (!assistantDiv) assistantDiv = addMsg('assistant', '<span class="role">Agent:</span>');
fullText += evt.delta;
assistantDiv.innerHTML = '<span class="role">Agent:</span>' + escapeHtml(fullText);
chat.scrollTop = chat.scrollHeight;
status.textContent = 'Streaming';
}
} catch {}
}
}
if (!fullText && !assistantDiv) addMsg('assistant', '<span class="role">Agent:</span><em>(empty)</em>');
status.textContent = '';
} catch (err) { status.textContent = 'Error: ' + err.message; }
if (validator.events.length > 0) {
try { const vr = await validator.validate(); chat.appendChild(validator.renderElement(vr)); chat.scrollTop = chat.scrollHeight; } catch {}
}
btn.disabled = false; input.focus();
});
</script>
</body>
</html>
""";
// ═══════════════════════════════════════════════════════════════════════
// SSE Validator Script (shared by all demo pages)
// ═══════════════════════════════════════════════════════════════════════
internal const string ValidationScript = """
// SseValidator - inline SSE stream validation for Foundry Responses demos
// Captures events during streaming and validates against the API behaviour contract.
(function() {
const style = document.createElement('style');
style.textContent = `
.sse-val { margin: .4rem 0 .6rem; padding: .3rem .5rem; font-size: .75rem; color: #aaa; border-top: 1px dashed #e8e8e8; }
.val-ok { color: #7ab88a; }
.val-err { color: #d47272; font-weight: 500; }
.val-issues { margin: .2rem 0; }
.val-issue { color: #c06060; font-size: .72rem; padding: .1rem 0; }
.val-issue b { color: #b04040; }
.val-at { color: #ccc; font-size: .68rem; }
.val-log summary { cursor: pointer; color: #bbb; font-size: .72rem; }
.val-log-items { max-height: 120px; overflow-y: auto; font-size: .7rem; background: #fafafa;
padding: .3rem; border-radius: 3px; margin-top: .15rem;
font-family: 'Cascadia Code', 'Fira Code', monospace; }
.val-i { color: #ccc; display: inline-block; width: 1.8rem; text-align: right; margin-right: .3rem; }
.val-t { color: #8ab4d0; }
`;
document.head.appendChild(style);
})();
class SseValidator {
constructor() { this.events = []; }
reset() { this.events = []; }
capture(eventType, data) { this.events.push({ eventType, data }); }
async validate() {
const resp = await fetch('/api/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ events: this.events })
});
return await resp.json();
}
renderElement(result) {
const el = document.createElement('div');
el.className = 'sse-val';
const n = result.eventCount;
const ok = result.isValid;
const vs = result.violations || [];
const esc = s => String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
let h = ok
? `<span class="val-ok">${n} events all rules passed </span>`
: `<span class="val-err">${n} events ${vs.length} violation(s)</span>`;
if (vs.length) {
h += '<div class="val-issues">';
vs.forEach(v => {
h += `<div class="val-issue"><b>[${esc(v.ruleId)}]</b> ${esc(v.message)} <span class="val-at">#${v.eventIndex}</span></div>`;
});
h += '</div>';
}
h += `<details class="val-log"><summary>Event log (${this.events.length})</summary><div class="val-log-items">`;
this.events.forEach((e, i) => {
h += `<div><span class="val-i">${i}</span> <span class="val-t">${esc(e.eventType)}</span></div>`;
});
h += '</div></details>';
el.innerHTML = h;
return el;
}
}
""";
}
@@ -0,0 +1,221 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates hosting agent-framework agents as Foundry Hosted Agents
// using the Azure AI Responses Server SDK.
//
// Demos:
// / - Homepage listing all demos
// /tool-demo - Agent with local tools + remote MCP tools
// /workflow-demo - Triage workflow routing to specialist agents
//
// Prerequisites:
// - Azure OpenAI resource with a deployed model
//
// Environment variables:
// - AZURE_OPENAI_ENDPOINT - your Azure OpenAI endpoint
// - AZURE_OPENAI_DEPLOYMENT - the model deployment name (default: "gpt-4o")
using System.ComponentModel;
using Azure.AI.OpenAI;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
// Load .env file if present (for local development)
Env.TraversePath().Load();
var builder = WebApplication.CreateBuilder(args);
// ---------------------------------------------------------------------------
// 1. Create the shared Azure OpenAI chat client
// ---------------------------------------------------------------------------
var endpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."));
var deployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o";
var azureClient = new AzureOpenAIClient(endpoint, new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential()));
IChatClient chatClient = azureClient.GetResponsesClient().AsIChatClient(deployment);
// ---------------------------------------------------------------------------
// 2. DEMO 1: Tool Agent — local tools + Microsoft Learn MCP
// ---------------------------------------------------------------------------
Console.WriteLine("Connecting to Microsoft Learn MCP server...");
McpClient mcpClient = await McpClient.CreateAsync(new HttpClientTransport(new()
{
Endpoint = new Uri("https://learn.microsoft.com/api/mcp"),
Name = "Microsoft Learn MCP",
}));
var mcpTools = await mcpClient.ListToolsAsync();
Console.WriteLine($"MCP tools available: {string.Join(", ", mcpTools.Select(t => t.Name))}");
builder.AddAIAgent(
name: "tool-agent",
instructions: """
You are a helpful assistant hosted as a Foundry Hosted Agent.
You have access to several tools - use them proactively:
- GetCurrentTime: Returns the current date/time in any timezone.
- GetWeather: Returns weather conditions for any location.
- Microsoft Learn MCP tools: Search and fetch Microsoft documentation.
When a user asks a technical question about Microsoft products, use the
documentation search tools to give accurate, up-to-date answers.
""",
chatClient: chatClient)
.WithAITool(AIFunctionFactory.Create(GetCurrentTime))
.WithAITool(AIFunctionFactory.Create(GetWeather))
.WithAITools(mcpTools.Cast<AITool>().ToArray());
// ---------------------------------------------------------------------------
// 3. DEMO 2: Triage Workflow — routes to specialist agents
// ---------------------------------------------------------------------------
ChatClientAgent triageAgent = new(
chatClient,
instructions: """
You are a triage agent that determines which specialist to hand off to.
Based on the user's question, ALWAYS hand off to one of the available agents.
Do NOT answer the question yourself - just route it.
""",
name: "triage_agent",
description: "Routes messages to the appropriate specialist agent");
ChatClientAgent codeExpert = new(
chatClient,
instructions: """
You are a coding and technology expert. You help with programming questions,
explain technical concepts, debug code, and suggest best practices.
Provide clear, well-structured answers with code examples when appropriate.
""",
name: "code_expert",
description: "Specialist agent for programming and technology questions");
ChatClientAgent creativeWriter = new(
chatClient,
instructions: """
You are a creative writing specialist. You help write stories, poems,
marketing copy, emails, and other creative content. You have a flair
for engaging language and vivid descriptions.
""",
name: "creative_writer",
description: "Specialist agent for creative writing and content tasks");
Workflow triageWorkflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(triageAgent)
.WithHandoffs(triageAgent, [codeExpert, creativeWriter])
.WithHandoffs([codeExpert, creativeWriter], triageAgent)
.Build();
builder.AddAIAgent("triage-workflow", (_, key) =>
triageWorkflow.AsAIAgent(name: key));
// Register triage-workflow as the non-keyed default so azd invoke (no model) works
builder.Services.AddSingleton(sp =>
sp.GetRequiredKeyedService<AIAgent>("triage-workflow"));
// ---------------------------------------------------------------------------
// 4. Wire up the agent-framework handler and Responses Server SDK
// ---------------------------------------------------------------------------
builder.Services.AddFoundryResponses();
var app = builder.Build();
// Dispose the MCP client on shutdown
app.Lifetime.ApplicationStopping.Register(() =>
mcpClient.DisposeAsync().AsTask().GetAwaiter().GetResult());
// ---------------------------------------------------------------------------
// 5. Routes
// ---------------------------------------------------------------------------
app.MapGet("/ready", () => Results.Ok("ready"));
app.MapFoundryResponses();
app.MapGet("/", () => Results.Content(Pages.Home, "text/html"));
app.MapGet("/tool-demo", () => Results.Content(Pages.ToolDemo, "text/html"));
app.MapGet("/workflow-demo", () => Results.Content(Pages.WorkflowDemo, "text/html"));
app.MapGet("/js/sse-validator.js", () => Results.Content(Pages.ValidationScript, "application/javascript"));
// Validation endpoint: accepts captured SSE lines and validates them
app.MapPost("/api/validate", (HostedWorkflowHandoff.CapturedSseStream captured) =>
{
var validator = new HostedWorkflowHandoff.ResponseStreamValidator();
foreach (var evt in captured.Events)
{
validator.ProcessEvent(evt.EventType, evt.Data);
}
validator.Complete();
return Results.Json(validator.GetResult());
});
app.Run();
// ---------------------------------------------------------------------------
// Local tool definitions
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Dev-only credential: reads a pre-fetched bearer token from AZURE_BEARER_TOKEN.
// When the value is missing or set to "DefaultAzureCredential", this credential
// throws CredentialUnavailableException so the ChainedTokenCredential falls
// through to DefaultAzureCredential.
// ---------------------------------------------------------------------------
[Description("Gets the current date and time in the specified timezone.")]
static string GetCurrentTime(
[Description("IANA timezone (e.g. 'America/New_York', 'Europe/London', 'UTC'). Defaults to UTC.")]
string timezone = "UTC")
{
try
{
var tz = TimeZoneInfo.FindSystemTimeZoneById(timezone);
return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz).ToString("F");
}
catch
{
return DateTime.UtcNow.ToString("F") + " (UTC - unknown timezone: " + timezone + ")";
}
}
[Description("Gets the current weather for a location. Returns temperature, conditions, and humidity.")]
static string GetWeather(
[Description("The city or location (e.g. 'Seattle', 'London, UK').")]
string location)
{
// Simulated weather - deterministic per location for demo consistency
var rng = new Random(location.ToUpperInvariant().GetHashCode());
var temp = rng.Next(-5, 35);
string[] conditions = ["sunny", "partly cloudy", "overcast", "rainy", "snowy", "windy", "foggy"];
var condition = conditions[rng.Next(conditions.Length)];
return $"Weather in {location}: {temp}C, {condition}. Humidity: {rng.Next(30, 90)}%. Wind: {rng.Next(5, 30)} km/h.";
}
internal sealed class DevTemporaryTokenCredential : TokenCredential
{
private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
private readonly string? _token;
public DevTemporaryTokenCredential()
{
this._token = Environment.GetEnvironmentVariable(EnvironmentVariable);
}
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> this.GetAccessToken();
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> new(this.GetAccessToken());
private AccessToken GetAccessToken()
{
if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential")
{
throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
}
return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1));
}
}
@@ -0,0 +1,126 @@
# Hosted-Workflow-Handoff
A hosted agent server demonstrating two patterns in a single app:
- **`tool-agent`** — an agent with local tools (time, weather) plus remote Microsoft Learn MCP tools
- **`triage-workflow`** — a handoff workflow that routes conversations to specialist agents (code expert or creative writer) using `AgentWorkflowBuilder`
Both agents are served over the Responses protocol. The server also exposes interactive web demos at `/tool-demo` and `/workflow-demo`.
> Unlike the other samples in this folder, this one connects to an **Azure OpenAI** resource directly (not an Azure AI Foundry project endpoint).
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- An Azure OpenAI resource with a deployed model (e.g., `gpt-4o`)
- Azure CLI logged in (`az login`)
## Configuration
Copy the template and fill in your values:
```bash
cp .env.example .env
```
Edit `.env`:
```env
AZURE_OPENAI_ENDPOINT=https://<your-account>.openai.azure.com/
AZURE_OPENAI_DEPLOYMENT=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
```
`AZURE_BEARER_TOKEN=DefaultAzureCredential` is a sentinel value that tells the app to skip the bearer token and fall through to `DefaultAzureCredential` (requires `az login`). Set it to a real token only when running in Docker.
> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
## Running directly (contributors)
```bash
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff
dotnet run
```
The server starts on `http://localhost:8088`. Open `http://localhost:8088` to see the demo index page.
### Test it
Using the Azure Developer CLI (invokes `triage-workflow` — the primary/default agent):
```bash
azd ai agent invoke --local "Write me a short poem about coding"
```
To target a specific agent by name, use curl:
```bash
# Invoke triage-workflow explicitly
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "Write me a haiku about autumn", "model": "triage-workflow"}'
```
```bash
# Invoke tool-agent (local tools + MCP)
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "What time is it in Tokyo?", "model": "tool-agent"}'
```
## Running with Docker
### 1. Publish for the container runtime
```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
```
### 2. Build the Docker image
```bash
docker build -f Dockerfile.contributor -t hosted-workflow-handoff .
```
### 3. Run the container
```bash
export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
docker run --rm -p 8088:8088 \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
--env-file .env \
hosted-workflow-handoff
```
### 4. Test it
```bash
azd ai agent invoke --local "Explain async/await in C#"
```
## How the triage workflow works
```
User message
┌──────────────┐
│ Triage Agent │ ──routes──▶ ┌─────────────┐
│ (router) │ │ Code Expert │
└──────────────┘ └─────────────┘
▲ │
│◀──────────────────────────────┘
└──routes──▶ ┌─────────────────┐
│ Creative Writer │
└─────────────────┘
```
The triage agent receives every message and hands off to the appropriate specialist. Specialists route back to the triage agent after responding, allowing for multi-turn conversations.
## NuGet package users
Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedWorkflowHandoff.csproj` for the `PackageReference` alternative.
@@ -0,0 +1,601 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Text.Json.Serialization;
namespace HostedWorkflowHandoff;
/// <summary>Captured SSE event for validation.</summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Instantiated by JSON deserialization")]
internal sealed record CapturedSseEvent(
[property: JsonPropertyName("eventType")] string EventType,
[property: JsonPropertyName("data")] string Data);
/// <summary>Captured SSE stream sent from the client for server-side validation.</summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Instantiated by JSON deserialization")]
internal sealed record CapturedSseStream(
[property: JsonPropertyName("events")] List<CapturedSseEvent> Events);
/// <summary>
/// Validates an SSE event stream from the Azure AI Responses Server SDK against
/// the API behaviour contract. Feed events sequentially via <see cref="ProcessEvent"/>
/// and call <see cref="Complete"/> when the stream ends.
/// </summary>
internal sealed class ResponseStreamValidator
{
private readonly List<ValidationViolation> _violations = [];
private int _eventCount;
private int _expectedSequenceNumber;
private StreamState _state = StreamState.Initial;
private string? _responseId;
private readonly HashSet<int> _addedItemIndices = [];
private readonly HashSet<int> _doneItemIndices = [];
private readonly HashSet<string> _addedContentParts = []; // "outputIdx:partIdx"
private readonly HashSet<string> _doneContentParts = [];
private readonly Dictionary<string, string> _textAccumulators = []; // "outputIdx:contentIdx" → accumulated text
private bool _hasTerminal;
/// <summary>All violations found so far.</summary>
internal IReadOnlyList<ValidationViolation> Violations => this._violations;
/// <summary>
/// Processes a single SSE event line pair (event type + JSON data).
/// </summary>
/// <param name="eventType">The SSE event type (e.g. "response.created").</param>
/// <param name="jsonData">The raw JSON data payload.</param>
internal void ProcessEvent(string eventType, string jsonData)
{
JsonElement data;
try
{
data = JsonDocument.Parse(jsonData).RootElement;
}
catch (JsonException ex)
{
this.Fail("PARSE-01", $"Invalid JSON in event data: {ex.Message}");
return;
}
this._eventCount++;
// ── Sequence number validation ──────────────────────────────────
if (data.TryGetProperty("sequence_number", out var seqProp) && seqProp.ValueKind == JsonValueKind.Number)
{
int seq = seqProp.GetInt32();
if (seq != this._expectedSequenceNumber)
{
this.Fail("SEQ-01", $"Expected sequence_number {this._expectedSequenceNumber}, got {seq}");
}
this._expectedSequenceNumber = seq + 1;
}
else if (this._state != StreamState.Initial || eventType != "error")
{
// Pre-creation error events may not have sequence_number
this.Fail("SEQ-02", $"Missing sequence_number on event '{eventType}'");
}
// ── Post-terminal guard ─────────────────────────────────────────
if (this._hasTerminal)
{
this.Fail("TERM-01", $"Event '{eventType}' received after terminal event");
return;
}
// ── Dispatch by event type ──────────────────────────────────────
switch (eventType)
{
case "response.created":
this.ValidateResponseCreated(data);
break;
case "response.queued":
this.ValidateStateTransition(eventType, StreamState.Created, StreamState.Queued);
this.ValidateResponseEnvelope(data, eventType);
break;
case "response.in_progress":
if (this._state is StreamState.Created or StreamState.Queued)
{
this._state = StreamState.InProgress;
}
else
{
this.Fail("ORDER-02", $"'response.in_progress' received in state {this._state} (expected Created or Queued)");
}
this.ValidateResponseEnvelope(data, eventType);
break;
case "response.output_item.added":
case "output_item.added":
this.ValidateInProgress(eventType);
this.ValidateOutputItemAdded(data);
break;
case "response.output_item.done":
case "output_item.done":
this.ValidateInProgress(eventType);
this.ValidateOutputItemDone(data);
break;
case "response.content_part.added":
case "content_part.added":
this.ValidateInProgress(eventType);
this.ValidateContentPartAdded(data);
break;
case "response.content_part.done":
case "content_part.done":
this.ValidateInProgress(eventType);
this.ValidateContentPartDone(data);
break;
case "response.output_text.delta":
case "output_text.delta":
this.ValidateInProgress(eventType);
this.ValidateTextDelta(data);
break;
case "response.output_text.done":
case "output_text.done":
this.ValidateInProgress(eventType);
this.ValidateTextDone(data);
break;
case "response.function_call_arguments.delta":
case "function_call_arguments.delta":
this.ValidateInProgress(eventType);
break;
case "response.function_call_arguments.done":
case "function_call_arguments.done":
this.ValidateInProgress(eventType);
break;
case "response.completed":
this.ValidateTerminal(data, "completed");
break;
case "response.failed":
this.ValidateTerminal(data, "failed");
break;
case "response.incomplete":
this.ValidateTerminal(data, "incomplete");
break;
case "error":
// Pre-creation error — standalone, no response.created precedes it
if (this._state != StreamState.Initial)
{
this.Fail("ERR-01", "'error' event received after response.created — should use response.failed instead");
}
this._hasTerminal = true;
break;
default:
// Unknown events are not violations — the spec may evolve
break;
}
}
/// <summary>
/// Call after the stream ends. Checks that a terminal event was received.
/// </summary>
internal void Complete()
{
if (!this._hasTerminal && this._state != StreamState.Initial)
{
this.Fail("TERM-02", "Stream ended without a terminal event (response.completed, response.failed, or response.incomplete)");
}
if (this._state == StreamState.Initial && this._eventCount == 0)
{
this.Fail("EMPTY-01", "No events received in the stream");
}
// Check for output items that were added but never completed
foreach (int idx in this._addedItemIndices)
{
if (!this._doneItemIndices.Contains(idx))
{
this.Fail("ITEM-03", $"Output item at index {idx} was added but never received output_item.done");
}
}
// Check for content parts that were added but never completed
foreach (string key in this._addedContentParts)
{
if (!this._doneContentParts.Contains(key))
{
this.Fail("CONTENT-03", $"Content part '{key}' was added but never received content_part.done");
}
}
}
/// <summary>
/// Returns a summary of all validation results.
/// </summary>
internal ValidationResult GetResult()
{
return new ValidationResult(
EventCount: this._eventCount,
IsValid: this._violations.Count == 0,
Violations: [.. this._violations]);
}
// ═══════════════════════════════════════════════════════════════════════
// Event-specific validators
// ═══════════════════════════════════════════════════════════════════════
private void ValidateResponseCreated(JsonElement data)
{
if (this._state != StreamState.Initial)
{
this.Fail("ORDER-01", $"'response.created' received in state {this._state} (expected Initial — must be first event)");
return;
}
this._state = StreamState.Created;
// Must have a response envelope
if (!data.TryGetProperty("response", out var resp))
{
this.Fail("FIELD-01", "'response.created' missing 'response' object");
return;
}
// Required response fields
this.ValidateRequiredResponseFields(resp, "response.created");
// Capture response ID for cross-event checks
if (resp.TryGetProperty("id", out var idProp))
{
this._responseId = idProp.GetString();
}
// Status must be non-terminal
if (resp.TryGetProperty("status", out var statusProp))
{
string? status = statusProp.GetString();
if (status is "completed" or "failed" or "incomplete" or "cancelled")
{
this.Fail("STATUS-01", $"'response.created' has terminal status '{status}' — must be 'queued' or 'in_progress'");
}
}
}
private void ValidateTerminal(JsonElement data, string expectedKind)
{
if (this._state is StreamState.Initial or StreamState.Created)
{
this.Fail("ORDER-03", $"Terminal event 'response.{expectedKind}' received before 'response.in_progress'");
}
this._hasTerminal = true;
this._state = StreamState.Terminal;
if (!data.TryGetProperty("response", out var resp))
{
this.Fail("FIELD-01", $"'response.{expectedKind}' missing 'response' object");
return;
}
this.ValidateRequiredResponseFields(resp, $"response.{expectedKind}");
if (resp.TryGetProperty("status", out var statusProp))
{
string? status = statusProp.GetString();
// completed_at validation (B6)
bool hasCompletedAt = resp.TryGetProperty("completed_at", out var catProp)
&& catProp.ValueKind != JsonValueKind.Null;
if (status == "completed" && !hasCompletedAt)
{
this.Fail("FIELD-02", "'completed_at' must be non-null when status is 'completed'");
}
if (status != "completed" && hasCompletedAt)
{
this.Fail("FIELD-03", $"'completed_at' must be null when status is '{status}'");
}
// error field validation
bool hasError = resp.TryGetProperty("error", out var errProp)
&& errProp.ValueKind != JsonValueKind.Null;
if (status == "failed" && !hasError)
{
this.Fail("FIELD-04", "'error' must be non-null when status is 'failed'");
}
if (status is "completed" or "incomplete" && hasError)
{
this.Fail("FIELD-05", $"'error' must be null when status is '{status}'");
}
// error structure validation
if (hasError)
{
this.ValidateErrorObject(errProp, $"response.{expectedKind}");
}
// cancelled output must be empty (B11)
if (status == "cancelled" && resp.TryGetProperty("output", out var outputProp)
&& outputProp.ValueKind == JsonValueKind.Array && outputProp.GetArrayLength() > 0)
{
this.Fail("CANCEL-01", "Cancelled response must have empty output array (B11)");
}
// response ID consistency
if (this._responseId is not null && resp.TryGetProperty("id", out var idProp)
&& idProp.GetString() != this._responseId)
{
this.Fail("ID-01", $"Response ID changed: was '{this._responseId}', now '{idProp.GetString()}'");
}
}
// Usage validation (optional, but if present must be structured correctly)
if (resp.TryGetProperty("usage", out var usageProp) && usageProp.ValueKind == JsonValueKind.Object)
{
this.ValidateUsage(usageProp, $"response.{expectedKind}");
}
}
private void ValidateOutputItemAdded(JsonElement data)
{
if (data.TryGetProperty("output_index", out var idxProp) && idxProp.ValueKind == JsonValueKind.Number)
{
int index = idxProp.GetInt32();
if (!this._addedItemIndices.Add(index))
{
this.Fail("ITEM-01", $"Duplicate output_item.added for output_index {index}");
}
}
else
{
this.Fail("FIELD-06", "output_item.added missing 'output_index' field");
}
if (!data.TryGetProperty("item", out _))
{
this.Fail("FIELD-07", "output_item.added missing 'item' object");
}
}
private void ValidateOutputItemDone(JsonElement data)
{
if (data.TryGetProperty("output_index", out var idxProp) && idxProp.ValueKind == JsonValueKind.Number)
{
int index = idxProp.GetInt32();
if (!this._addedItemIndices.Contains(index))
{
this.Fail("ITEM-02", $"output_item.done for output_index {index} without preceding output_item.added");
}
this._doneItemIndices.Add(index);
}
else
{
this.Fail("FIELD-06", "output_item.done missing 'output_index' field");
}
}
private void ValidateContentPartAdded(JsonElement data)
{
string key = GetContentPartKey(data);
if (!this._addedContentParts.Add(key))
{
this.Fail("CONTENT-01", $"Duplicate content_part.added for {key}");
}
}
private void ValidateContentPartDone(JsonElement data)
{
string key = GetContentPartKey(data);
if (!this._addedContentParts.Contains(key))
{
this.Fail("CONTENT-02", $"content_part.done for {key} without preceding content_part.added");
}
this._doneContentParts.Add(key);
}
private void ValidateTextDelta(JsonElement data)
{
string key = GetTextKey(data);
string delta = data.TryGetProperty("delta", out var deltaProp)
? deltaProp.GetString() ?? string.Empty
: string.Empty;
if (!this._textAccumulators.TryGetValue(key, out string? existing))
{
this._textAccumulators[key] = delta;
}
else
{
this._textAccumulators[key] = existing + delta;
}
}
private void ValidateTextDone(JsonElement data)
{
string key = GetTextKey(data);
string? finalText = data.TryGetProperty("text", out var textProp)
? textProp.GetString()
: null;
if (finalText is null)
{
this.Fail("TEXT-01", $"output_text.done for {key} missing 'text' field");
return;
}
if (this._textAccumulators.TryGetValue(key, out string? accumulated) && accumulated != finalText)
{
this.Fail("TEXT-02", $"output_text.done text for {key} does not match accumulated deltas (accumulated {accumulated.Length} chars, done has {finalText.Length} chars)");
}
}
// ═══════════════════════════════════════════════════════════════════════
// Shared field validators
// ═══════════════════════════════════════════════════════════════════════
private void ValidateRequiredResponseFields(JsonElement resp, string context)
{
if (!HasNonNullString(resp, "id"))
{
this.Fail("FIELD-01", $"{context}: response missing 'id'");
}
if (resp.TryGetProperty("object", out var objProp))
{
if (objProp.GetString() != "response")
{
this.Fail("FIELD-08", $"{context}: response.object must be 'response', got '{objProp.GetString()}'");
}
}
else
{
this.Fail("FIELD-08", $"{context}: response missing 'object' field");
}
if (!resp.TryGetProperty("created_at", out var catProp) || catProp.ValueKind == JsonValueKind.Null)
{
this.Fail("FIELD-09", $"{context}: response missing 'created_at'");
}
if (!resp.TryGetProperty("status", out _))
{
this.Fail("FIELD-10", $"{context}: response missing 'status'");
}
if (!resp.TryGetProperty("output", out var outputProp) || outputProp.ValueKind != JsonValueKind.Array)
{
this.Fail("FIELD-11", $"{context}: response missing 'output' array");
}
}
private void ValidateErrorObject(JsonElement error, string context)
{
if (!HasNonNullString(error, "code"))
{
this.Fail("ERR-02", $"{context}: error object missing 'code' field");
}
if (!HasNonNullString(error, "message"))
{
this.Fail("ERR-03", $"{context}: error object missing 'message' field");
}
}
private void ValidateUsage(JsonElement usage, string context)
{
if (!usage.TryGetProperty("input_tokens", out _))
{
this.Fail("USAGE-01", $"{context}: usage missing 'input_tokens'");
}
if (!usage.TryGetProperty("output_tokens", out _))
{
this.Fail("USAGE-02", $"{context}: usage missing 'output_tokens'");
}
if (!usage.TryGetProperty("total_tokens", out _))
{
this.Fail("USAGE-03", $"{context}: usage missing 'total_tokens'");
}
}
private void ValidateResponseEnvelope(JsonElement data, string eventType)
{
if (!data.TryGetProperty("response", out var resp))
{
this.Fail("FIELD-01", $"'{eventType}' missing 'response' object");
return;
}
this.ValidateRequiredResponseFields(resp, eventType);
// Response ID consistency
if (this._responseId is not null && resp.TryGetProperty("id", out var idProp)
&& idProp.GetString() != this._responseId)
{
this.Fail("ID-01", $"Response ID changed: was '{this._responseId}', now '{idProp.GetString()}'");
}
}
// ═══════════════════════════════════════════════════════════════════════
// Helpers
// ═══════════════════════════════════════════════════════════════════════
private void ValidateInProgress(string eventType)
{
if (this._state != StreamState.InProgress)
{
this.Fail("ORDER-04", $"'{eventType}' received in state {this._state} (expected InProgress)");
}
}
private void ValidateStateTransition(string eventType, StreamState expected, StreamState next)
{
if (this._state != expected)
{
this.Fail("ORDER-05", $"'{eventType}' received in state {this._state} (expected {expected})");
}
else
{
this._state = next;
}
}
private void Fail(string ruleId, string message)
{
this._violations.Add(new ValidationViolation(ruleId, message, this._eventCount));
}
private static bool HasNonNullString(JsonElement obj, string property)
{
return obj.TryGetProperty(property, out var prop)
&& prop.ValueKind == JsonValueKind.String
&& !string.IsNullOrEmpty(prop.GetString());
}
private static string GetContentPartKey(JsonElement data)
{
int outputIdx = data.TryGetProperty("output_index", out var oi) ? oi.GetInt32() : -1;
int partIdx = data.TryGetProperty("content_index", out var pi) ? pi.GetInt32() : -1;
return $"{outputIdx}:{partIdx}";
}
private static string GetTextKey(JsonElement data)
{
int outputIdx = data.TryGetProperty("output_index", out var oi) ? oi.GetInt32() : -1;
int contentIdx = data.TryGetProperty("content_index", out var ci) ? ci.GetInt32() : -1;
return $"{outputIdx}:{contentIdx}";
}
private enum StreamState
{
Initial,
Created,
Queued,
InProgress,
Terminal,
}
}
/// <summary>A single validation violation.</summary>
/// <param name="RuleId">The rule identifier (e.g. SEQ-01, FIELD-02).</param>
/// <param name="Message">Human-readable description of the violation.</param>
/// <param name="EventIndex">1-based index of the event that triggered this violation.</param>
internal sealed record ValidationViolation(string RuleId, string Message, int EventIndex);
/// <summary>Overall validation result.</summary>
/// <param name="EventCount">Total number of events processed.</param>
/// <param name="IsValid">True if no violations were found.</param>
/// <param name="Violations">List of all violations.</param>
internal sealed record ValidationResult(int EventCount, bool IsValid, IReadOnlyList<ValidationViolation> Violations);
@@ -0,0 +1,30 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: triage-workflow
displayName: "Triage Handoff Workflow Agent"
description: >
A hosted agent demonstrating two patterns in a single server: a tool-equipped agent
with local tools and remote MCP tools, and a triage workflow that routes conversations
to specialist agents (code expert or creative writer) via handoff orchestration.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Workflows
- Handoff
- Agent Framework
template:
name: triage-workflow
kind: hosted
protocols:
- protocol: responses
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
parameters:
properties: []
resources: []
@@ -0,0 +1,9 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: triage-workflow
protocols:
- protocol: responses
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi

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