Compare commits

...

102 Commits

Author SHA1 Message Date
Tao Chen c5e1acf561 Fix formatting 2026-06-26 17:15:08 -07:00
Tao Chen c508c42220 Improve comments 2026-06-26 15:39:20 -07:00
Tao Chen b8d34dc482 Use lazy-None init for the event queue, matching the executor lock
Initialize _event_queue to None and create it on first use in _get_event_queue, mirroring the per-executor lock. Avoids constructing a queue in __init__/reset_for_new_run that is immediately discarded once the running loop is known.
2026-06-26 15:24:53 -07:00
Tao Chen 1e12403759 Re-create runner context event queue lazily under the running loop
Like the per-executor lock, the runner context's asyncio.Queue bound to the first event loop it was awaited under, so reusing a workflow across loops (e.g. successive asyncio.run calls) raised 'bound to a different event loop'. Re-create the queue lazily via _get_event_queue() when the running loop changes. Adds an integration test reusing a workflow across event loops.
2026-06-26 14:37:16 -07:00
Tao Chen 5ed5d7ab7c Create per-executor lock lazily under the running loop
asyncio.Lock created in Executor.__init__ would bind to the first event loop it was awaited under, so reusing an executor/workflow across loops (e.g. successive asyncio.run calls) raised 'bound to a different event loop'. Create the lock lazily via _get_execution_lock(), re-creating it when the running loop changes. Adds a loop-scoped lock test.
2026-06-26 13:46:43 -07:00
Tao Chen 252735a604 Process messages to an executor serially within a superstep
Add a per-executor asyncio.Lock in Executor.execute so each executor processes its messages one at a time within a superstep, while preserving concurrency across distinct executors. Includes a regression test.
2026-06-26 11:59:49 -07:00
Tao Chen 3c3feb8705 Python: Refactor runner/workflow responsibilities and fix checkpoint ancestry bug (#6695)
* Refactor runner/workflow responsibilities, add concurrency guards, and fix checkpoint ancestry bug

Move runner-state ownership out of Workflow into Runner for clearer responsibilities. Add a weakref-based concurrent-run guard in Workflow and fix the stream-drop race in run_until_convergence. Fix the checkpoint ancestry bug by tracking the previous checkpoint id as runner instance state so parent pointers persist across resumed runs. Move Runner to a deprecated lazy __getattr__ export (backward-compatible with DeprecationWarning) and export CheckpointID.

* Scope runtime checkpoint storage to its owning run

Close the stream-drop race where a dropped run's deferred async-generator finalizer could leave a runtime checkpoint storage override set (inherited by a new run) or clear a successor run's storage. run() now defensively clears any stale override before starting, and _run_core only clears the override if this run still owns it (mirroring the _active_run ownership guard). Adds regression tests for both the inheritance and clobber cases.

* Collapse runtime-storage ownership into the active-run weakref

_runtime_storage_owner always held the same weakref as _active_run, so the two ownership conditions were equivalent. Derive ownership from a single owns_run = (_active_run is my_active_run) captured before the active-run clear, and remove the redundant field. No behavior change.

* Nest runtime-storage clear under the owns_run guard

Both the active-run release and the runtime-storage clear are gated on owns_run, so fold the storage clear inside the if owns_run block. No behavior change.

* Reset resume flag in a finally so it can't leak across runs

_resumed_from_checkpoint was only cleared on the success path of run_until_convergence, so a failure during a resumed run (e.g. executor failure) left it True. The next fresh run then skipped the superstep-0 checkpoint and parented later checkpoints to the stale resume point. Move the reset into a finally. Add a regression test that fails a resumed run via an executor error and asserts the next fresh run creates the superstep-0 checkpoint.

* Fix tests and formatting

* Fix formatting

* Address comments

* Update type ignore statements
2026-06-25 23:15:01 +00:00
Tao Chen b8b43798b9 Python: Update FHA with toolbox sample with more auth methods (#6713)
* Update FHA with toolbox sample with more auth methods

* Clean up

* Clean up other samples
2026-06-25 22:59:15 +00:00
Peter Ibekwe d9ec5eaab4 update package version (#6752) 2026-06-25 22:06:53 +00:00
SergeyMenshykh e3b64fdc47 .NET: [BREAKING] Make all AgentSkillsProvider tools require approval by default (#6729)
* Make all AgentSkillsProvider tools require approval by default

- Wrap all tools (load_skill, read_skill_resource, run_skill_script) with
  ApprovalRequiredAIFunction unconditionally
- Add ReadOnlyToolsAutoApprovalRule and AllToolsAutoApprovalRule static
  properties following the FileAccessProvider pattern
- Remove ScriptApproval from AgentSkillsProviderOptions and
  UseScriptApproval from AgentSkillsProviderBuilder
- Add Agent_Step07_SkillsAutoApproval sample

Closes #6727

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

* Add UseToolApproval to hosted AgentSkills scenarios

Wire AllToolsAutoApprovalRule into the integration test container and
the Hosted-AgentSkills sample so skill tools execute without blocking
on approval when no interactive approval handler is configured.

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

* Add API compatibility suppressions for removed ScriptApproval members

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

---------

Co-authored-by: SergeyMenshykh <SergeMenshikh@outlook.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 20:44:12 +00:00
Peter Ibekwe 3a5bbb5f8e .NET: Add DeclarativeWorkflowJsonOptions for AOT-safe declarative workflow checkpointing (#6745)
* Add experimental DeclarativeWorkflowJsonOptions for AOT-safe declarative workflow checkpointing

* Address PR comments
2026-06-25 19:57:22 +00:00
Peter Ibekwe a7332d69a8 .NET: Fix issue with resuming checkpoint after package version upgrade (#6670)
* Fix issue with resuming checkpoint after package version upgrade

* Address PR comments

* Fix changelog encoding
2026-06-25 18:32:22 +00:00
dependabot[bot] e57e9455b3 Build(deps): Bump hyperlight-sandbox-python-guest in /python (#6737)
Bumps hyperlight-sandbox-python-guest from 0.4.0 to 0.5.0.

---
updated-dependencies:
- dependency-name: hyperlight-sandbox-python-guest
  dependency-version: 0.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-25 17:41:39 +00:00
dependabot[bot] d97bc4fe39 Build(deps): Bump huggingface-hub from 1.20.1 to 1.21.0 in /python (#6738)
Bumps [huggingface-hub](https://github.com/huggingface/huggingface_hub) from 1.20.1 to 1.21.0.
- [Release notes](https://github.com/huggingface/huggingface_hub/releases)
- [Commits](https://github.com/huggingface/huggingface_hub/compare/v1.20.1...v1.21.0)

---
updated-dependencies:
- dependency-name: huggingface-hub
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-25 17:41:32 +00:00
dependabot[bot] 5d57b10b9f Build(deps): Bump google-genai from 1.75.0 to 2.10.0 in /python (#6739)
Bumps [google-genai](https://github.com/googleapis/python-genai) from 1.75.0 to 2.10.0.
- [Release notes](https://github.com/googleapis/python-genai/releases)
- [Changelog](https://github.com/googleapis/python-genai/blob/main/CHANGELOG.md)
- [Commits](https://github.com/googleapis/python-genai/compare/v1.75.0...v2.10.0)

---
updated-dependencies:
- dependency-name: google-genai
  dependency-version: 2.10.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-25 17:41:22 +00:00
dependabot[bot] 8cd71dd4f6 Build(deps): Bump fastapi from 0.124.4 to 0.138.0 in /python (#6740)
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.124.4 to 0.138.0.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.124.4...0.138.0)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-version: 0.138.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-25 17:41:13 +00:00
SergeyMenshykh 5e5dd87c91 Update .NET SDK to 10.0.301 (#6730)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 15:27:54 +00:00
SergeyMenshykh d0be98d649 Remove {resource_instructions} and {script_instructions} placeholder mechanism (#6706)
Embed resource and script instruction text directly in the default
prompt template instead of using placeholder substitution. Custom
templates now only need the {skills} placeholder.

Co-authored-by: SergeyMenshykh <SergeMenshikh@outlook.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 15:19:35 +00:00
SergeyMenshykh 802fe13053 Disable failing durable function integration tests (#6731)
Skip LongRunningToolsSampleValidationAsync and ReliableStreamingSampleValidationAsync
tests that are persistently failing in CI.

Co-authored-by: SergeyMenshykh <SergeMenshikh@outlook.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 14:42:34 +00:00
Eduard van Valkenburg d75f2286f4 Python: Add Telegram channel for agent-framework-hosting (#6698)
* Python: Add Telegram channel for agent-framework-hosting

- Add agent-framework-hosting-telegram package with TelegramChannel
  supporting polling and webhook transports, streaming edits with
  Telegram Bot API rate limiting, per-chat serial workers, and
  multi-modal inbound/outbound (text, photo, document, voice)
- Add local_telegram sample demonstrating multi-channel hosting with
  a TelegramChannel alongside ResponsesChannel, using per-chat
  FileHistoryProvider and a run_hook for Telegram persona temperature
- Fix test layout: move tests to tests/hosting_telegram/ (no __init__.py)
- Remove old [tool.mypy] section and mypy poe task; source type-checking
  is handled by pyright via shared_tasks
- Update uv.lock, pyproject.toml workspace sources, and PACKAGE_STATUS.md

Fixes #6588
Refs #6265

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

* Python: Address Telegram channel CI failures and review feedback

- Fix webhook secret validation to use constant-time compare_digest
- Harden webhook update parsing: require integer chat IDs and guard slash-only commands
- Fix streaming edge cases in TelegramChannel:
  - prevent edit worker deadlocks when text exceeds 4096 chars
  - prevent deadlock when placeholder send fails (message_id stays None)
  - enforce edit throttling with minimum interval sleep
  - honor send_typing_action=False in streaming mode
  - always forward final multimodal output (e.g. images), while avoiding duplicate text sends
- Expand Telegram tests for slash-only command handling, non-int chat IDs, and streaming behavior (long text, final images, typing toggle)
- Fix sample/docs feedback:
  - rename sample package to agent-framework-hosting-sample-local-telegram
  - switch sample uv.sources from feature branch to main
  - align docs/tool names with lookup_weather
  - fix broken links and server run instructions in README/call_server.py
  - align local_telegram app docstrings with reasoning hook behavior and strip model in responses_hook

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

* Python: Fix TelegramChannel streaming to iterate contents for multimodal support

- Remove stale PR reference from module docstring
- Add Google-style docstring to TelegramChannel.__init__ documenting all keyword args
- Fix _stream_to_chat to iterate update.contents instead of using
  getattr(update, 'text', None); text chunks are extracted from Content
  items with type='text', non-text content in updates is correctly
  ignored (images etc. are forwarded via the final response)
- Update _FakeStreamUpdate test helper to use contents list matching the
  real AgentResponseUpdate API; add from_text/from_image class methods
- Update _FakeResponseStream to accept _FakeStreamUpdate objects directly
- Add test verifying multimodal stream updates don't corrupt text accumulator

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

* Python: Split local_telegram into simple Telegram-only and new multi-channel sample

local_telegram is now a focused Telegram-only sample:
- Removes ResponsesChannel and all responses_hook code
- Removes call_server.py (no HTTP endpoint to call)
- Uses a deterministic lookup_weather tool (hash-based, not random)
- Single run_hook that strips model and raises reasoning effort
- Drops agent-framework-hosting-responses dependency

New local_multi_channel sample shows running both channels at once:
- ResponsesChannel + TelegramChannel sharing a FileHistoryProvider
- Cross-channel session resumption via previous_response_id
- call_server.py moved here (the Responses endpoint lives here now)
- Demonstrates the multi-channel coordination story

Update README table to list both samples with clear descriptions.

Also delete personal_assistant/.venv which was not tracked but caused
pyright to crawl the entire installed venv (thousands of files),
making sample pyright checks hang indefinitely.

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

* Python: Fallback when Telegram final edit fails

- only mark final edit as sent after a confirmed 2xx edit response
- fall back to sendMessage when final edit returns a non-success status
- add regression test covering failed final edit fallback behavior

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

* Python: Fix optional await_args typing in telegram test

- assert await_args is not None before reading kwargs in streaming fallback test
- resolves test-typing failures across mypy/pyright/ty/zuban for hosting-telegram

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 11:40:48 +00:00
Eduard van Valkenburg ce74c84bdb Python: Preserve OTel parent context for deferred streams (#6709)
* Python: Preserve OTel parent context for deferred streams

- capture current OTel context when opening host-managed streaming runs
- re-activate captured context during deferred stream pulls and finalization
- add host-level regression coverage for deferred stream parent-span linkage
- add Responses channel integration coverage for request parent span propagation

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

* Python: Capture OTel stream context before target.run

- capture OTel context snapshot before invoking target.run in _invoke_stream
- add regression test guarding capture-before-run evaluation order

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 05:51:18 +00:00
Eduard van Valkenburg 4fb1fb615a Python: Fix Hyperlight CodeAct span parenting (#6712)
* Fix Hyperlight CodeAct span parenting

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

* Fix Hyperlight test OTEL fixture

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

* Fix Hyperlight test typing annotations

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 05:50:58 +00:00
Ben Thomas 41a9c54bbe Add Foundry project environment variables (#6721) 2026-06-24 15:18:59 -07:00
Giles Odigwe 9f1ee23a4b Python: [BREAKING] Refactor FileSkillsSource for depth-based discovery and predicate filters (#6488)
* Python: [Breaking] Refactor FileSkillsSource for depth-based discovery and predicate filters

Refactors FileSkillsSource to make script and resource discovery more flexible.

## Changes

- **Drops** resource_directories / script_directories options (preconfigured
  directory whitelists).
- **Adds** search_depth option (>= 1, default 2): controls how deep the
  recursive scan goes within each skill directory.
- **Adds** script_filter / resource_filter predicate options that receive a
  FileSkillFilterContext (skill_name + relative_file_path), allowing
  whitelist/blacklist filtering by file path.
- **Adds** FileSkillFilterContext class exported from agent_framework.

## Notes

- The Skills API is marked @experimental -- the option removals are intentional
  breaking changes within the experimental surface.
- Security checks (path containment, symlink detection) are preserved and
  continue to use the skill root directory as the trusted boundary.
- Ports the same refactoring from .NET PR #6109 while following Python
  conventions (instance methods, Callable type hints, __slots__).

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

* Address PR feedback: clarify depth constants and skip nested skill directories

- Add clarifying comments distinguishing MAX_SEARCH_DEPTH (SKILL.md
  discovery) from DEFAULT_SEARCH_DEPTH (per-skill resource/script scanning).
- Stop recursing into subdirectories that contain their own SKILL.md,
  preventing child skill files from being attached to the parent skill.
- Add test verifying nested skill boundary is respected.

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

* Remove __slots__ from FileSkillFilterContext and add type-ignore comments

- Remove __slots__ from FileSkillFilterContext per reviewer feedback —
  the optimization is negligible and inconsistent with sibling classes.
- Add type: ignore[attr-defined] / ty: ignore[unresolved-attribute]
  comments to test lines accessing private _resources/_scripts attributes,
  matching the convention established on main.

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

* Simplify filter predicates: remove FileSkillFilterContext, use Callable[[str, str], bool]

Address reviewer feedback:
- Remove FileSkillFilterContext class — a dedicated class for two strings
  is overkill in Python. Filters now receive (skill_name, relative_file_path)
  directly as positional args.
- Update docstrings to describe behavior instead of referencing private
  instance attributes.
- Remove FileSkillFilterContext from exports and __all__.
- Update all test lambdas and remove TestFileSkillFilterContext class.

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

* Use DEFAULT_SEARCH_DEPTH as default argument directly

Instead of accepting int | None and resolving None to the default
internally, use DEFAULT_SEARCH_DEPTH as the parameter default value
on both FileSkillsSource.__init__() and SkillsProvider.from_paths().

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 21:48:25 +00:00
Ben Thomas 336a19fd32 .NET: .NET samples: migrate coding samples to Foundry-first AIProjectClient (#6557)
* Migrate 02-agents/Agents samples to AIProjectClient (Foundry)

Replace AzureOpenAIClient with AIProjectClient as the AI provider in all
02-agents/Agents samples, aligning with the Foundry-first approach.

Changes:
- 19 Program.cs files migrated to use AIProjectClient.AsAIAgent()
- 19 .csproj files updated (Azure.AI.OpenAI -> Microsoft.Agents.AI.Foundry)
- Environment variables: AZURE_OPENAI_* -> FOUNDRY_PROJECT_ENDPOINT/FOUNDRY_MODEL
- Updated description comments to reflect Foundry backend
- Provider-specific samples in AgentsWithFoundry/ intentionally unchanged

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

* Migrate 02-agents/AgentSkills, AgentWithMemory, AgentWithRAG, AgentOpenTelemetry to AIProjectClient

Replace AzureOpenAIClient with AIProjectClient as the AI provider.
Environment variables: AZURE_OPENAI_* -> FOUNDRY_PROJECT_ENDPOINT/FOUNDRY_MODEL.

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

* Migrate 03-workflows samples to AIProjectClient (Foundry)

Replace AzureOpenAIClient with AIProjectClient as the AI provider in
all 03-workflows samples that use an AI model.
Environment variables: AZURE_OPENAI_* -> FOUNDRY_PROJECT_ENDPOINT/FOUNDRY_MODEL.

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

* Fix PR 6557 build breaks and align Foundry client usage

- Add explicit Azure.Identity package references to migrated sample projects
  that use DefaultAzureCredential
- Fix AgentWithRAG_Step05_Neo4jGraphRAG to use AIProjectClient.AsAIAgent()
  with ChatOptions.ModelId instead of AIProjectClient.AsIChatClient()
- Keep migrated samples on AIProjectClient pattern (no FoundryAgent/AzureOpenAIClient)

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

* Address PR 6557 Foundry review follow-ups

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

* Fix post-rebase sample build and format regressions

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

* Updates to fix issues from switching to Responses.

* Fixing more tests and deleting checkpoint directories created for samples.

* Fixing formatting

* Restore DefaultAzureCredential warnings in agents samples

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 13:45:14 -07:00
Roger Barreto 0283fd00a1 .NET: Fix hosted agent crash after tool call by rooting session store under $HOME (#6231) (#6714)
* .NET: Fix hosted agent crash after tool call by rooting session store under $HOME

FileSystemAgentSessionStore.CreateDefault rooted the hosted session store at the
filesystem root "/.checkpoints", which is read-only inside a Foundry hosted
container. After a local tool call the response handler persists the session, so
the write to "/.checkpoints" threw IOException and tore down the container, which
the platform surfaced as "mount: /app: mount failed: No such file or directory.".

Root the hosted store at $HOME (default /home/session), the only writable and
durable location per the container image spec. Persistence failures stay fatal but
are now wrapped in a clear, actionable IOException instead of the opaque raw error.

Add unit tests covering hosted and local path resolution plus the clear error, and
enable the ToolCalling Foundry Hosted Agents integration tests (verified live).

Fixes #6231

* .NET: Harden hosted session store against a filesystem-root HOME

Address review feedback on #6714: a misconfigured HOME pointing at a filesystem
root (e.g. "/") resolved back to "/.checkpoints" and would reintroduce the original
read-only-root crash. CreateDefault now falls back to the default session-data
directory (/home/session) when HOME is missing, blank, a filesystem root, or an
unnormalizable path. Adds a unit test locking in the "never the filesystem root"
behavior for a hosted HOME of "/".

Related #6231
2026-06-24 18:55:08 +00:00
Eduard van Valkenburg 5627dc0493 docs: Add Python session identity ADR (#6630)
* docs: Add Python session identity ADR

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

* docs: Clarify session identity ADR example

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

* docs: Reorder session identity options

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

* docs: Select richer service session identity option

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

* docs: Accept Python session identity ADR

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

* docs: clarify ADR session identity lifecycle

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

* docs: fix ADR concrete gap framing

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

* docs: refine ADR identity decision guide

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 17:42:45 +00:00
Yufeng He 1df47667ea Python: surface cache and reasoning token counts for the Bedrock and Gemini connectors (#6640)
* Python: surface Gemini cached and thinking token counts in usage details

* Python: surface Bedrock cache token counts in usage details

* Python: surface Gemini cached and thinking token counts in usage details

* Python: surface Bedrock cache token counts in usage details

* Return None from Bedrock _parse_usage when no token counts are present

Matches the UsageDetails | None return annotation and the Gemini
connector's behavior, so a usage payload with no recognized keys no
longer propagates an empty mapping. Adds a regression test.
2026-06-24 17:09:01 +00:00
Giles Odigwe 91f639a694 Python: Explicitly emit available_resources and available_scripts in skill content (#6694)
Skill content now always emits <available_resources> and <available_scripts>
blocks, using self-closing elements when empty, so models receive an
authoritative list per category and do not hallucinate resource/script names.
FileSkill now also emits its resources block.

Closes #6348

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 15:42:58 +00:00
Roger Barreto a9e5f6d798 .NET: .NET Foundry: add CreateMcpTool projectConnectionId overload (#6703)
* .NET Foundry: add CreateMcpTool projectConnectionId overload

Adds FoundryAITool.CreateMcpTool(serverLabel, serverUri, projectConnectionId, ...)
so hosted MCP tools can authenticate through a Foundry project connection, matching
the Python FoundryChatClient.get_mcp_tool(..., project_connection_id=...) factory.

The connection id is applied via the McpTool.ProjectConnectionId extension that ships
in Azure.AI.Projects.Agents (patches project_connection_id), already referenced by the
Foundry package. Includes unit tests and sample/README guidance plus the existing
FromResponseTool workaround.

* Fold projectConnectionId into existing CreateMcpTool overload

Replaces the separate project-connection overload with an optional
projectConnectionId parameter on the existing serverUri CreateMcpTool, so all
settings (authorizationToken, headers, allowedTools, ...) stay available and there
is no positional overload ambiguity. Adds tests for the default (no connection)
path and for preserving other settings. Sample/README now show only the supported
overload.
2026-06-24 12:40:47 +00:00
SergeyMenshykh ea7ae1cc00 .NET: [BREAKING] Support archive-type skills in AgentMcpSkillsSource (#6631)
* NET: Support archive-type skills in AgentMcpSkillsSource

Add archive-type skill discovery to the MCP skills source. Index entries
are dispatched to per-type loaders (skill-md and archive) via a new
IMcpSkillEntryLoader strategy. The archive loader downloads, safely
unpacks, and serves packaged skills through an internal file skills
source, while ensuring MCP-delivered scripts are never executed.

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

* Fix CS0121 ambiguity in UseSource null test

Cast null! to AgentSkillsSource to disambiguate from the new
Func<ILoggerFactory?, AgentSkillsSource> overload.

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

* Address PR review: fix misleading comment and catch UnauthorizedAccessException in Dispose

- Remove hardcoded '50' from test comment; it now says 'default cap'
  without citing a specific number that can drift from the constant.
- Catch UnauthorizedAccessException alongside IOException in test
  Dispose for robust cleanup.

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

* Decouple shared refresh from per-caller cancellation

Use CancellationToken.None for the shared refresh so one caller's
cancellation does not abort work for all concurrent waiters. Waiters
use WaitAsync(cancellationToken) to cancel independently. The refresh
owner checks its own token after publishing the result.

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

* Fix file encoding: add UTF-8 BOM to archive tests

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

* Fix file encoding: add UTF-8 BOM to ArchiveFormat.cs

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

* Clarify pruning doc: covers non-actionable entries too

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

* Add branch-coverage tests and drop [Experimental] attribute

- Add 5 unit tests covering FilterValidEntries/download condition branches
  (missing name, invalid name chars, missing url, unsupported format, text-only blob)
- Remove [Experimental] attribute from AgentMcpSkillsSourceOptions (alpha package suffices)

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

---------

Co-authored-by: SergeyMenshykh <SergeMenshikh@outlook.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 13:12:08 +01:00
SergeyMenshykh e049bb5691 .NET: Add IncludeDetailedErrors option for skill script execution (#6680)
* fix: propagate skill script/resource exceptions instead of swallowing them

Stop catching and returning generic error strings in RunSkillScriptAsync and
ReadSkillResourceAsync. Exceptions are now logged and rethrown so that
FunctionInvokingChatClient can decide whether to surface details to the model
via its existing IncludeDetailedErrors option (default: safe generic message).

Fixes microsoft/agent-framework#6304

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

* Add IncludeDetailedErrors option for skill script execution

Add an IncludeDetailedErrors option to AgentSkillsProviderOptions. When enabled,
RunSkillScriptAsync appends the exception message to the error returned to the
model so it can self-correct (e.g. retry with different arguments). When
disabled (default), the exception is logged and rethrown, letting
FunctionInvokingChatClient apply its own IncludeDetailedErrors policy.

ReadSkillResourceAsync now logs and rethrows as well, since resources take no
arguments and a generic swallowed error is not actionable by the model.

Fixes microsoft/agent-framework#6304

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

* Add prompt-injection caution to IncludeDetailedErrors doc

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

---------

Co-authored-by: SergeyMenshykh <SergeMenshikh@outlook.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 11:59:55 +01:00
SergeyMenshykh dd4b7ff475 .NET: Fix SearchDirectoriesForSkills to stop recursing after finding SKILL.md (#6686)
* Fix SearchDirectoriesForSkills to stop recursing after finding SKILL.md

When a directory contains SKILL.md, subdirectories are part of that skill
and should not be treated as independent skill roots. Add a return after
adding the directory to results to prevent incorrect recursion.

Also adds a regression test verifying nested SKILL.md files are not
discovered as separate skills.

Fixes microsoft/agent-framework#6683

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

* Fix test: use matching directory name so nested SKILL.md would pass validation

The child skill's frontmatter name must match its directory name,
otherwise it gets rejected by validation regardless of the recursion fix.
This ensures the test actually validates the stop-recursing behavior.

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

---------

Co-authored-by: SergeyMenshykh <SergeMenshikh@outlook.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 08:35:11 +00:00
Taisir Hassan d5c15f2fe1 .NET/Python: Purview: prefer token principal for user identity (#6693)
* Purview: prefer token principal for user identity

Align Purview middleware identity resolution so user-token principals are preferred before supplied message identities, while app-token flows continue to use validated fallback user IDs. Also fix the content activities user route and add regression coverage for identity precedence and route construction.

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

* .NET: Fix user ID resolution logic in ScopedContentProcessor and add unit test for empty token user ID

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 00:16:44 +00:00
Eduard van Valkenburg 4cf7ace446 Python: track dependency maintenance PR creation (#6665)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 00:09:56 +00:00
Amit Dhawan 5fff0df2af Python: Add load_dotenv to get-started samples and fix chat_response_… (#6691)
* Python: Add load_dotenv to get-started samples and fix chat_response_cancellation docs

* Potential fix for pull request finding

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

---------

Co-authored-by: Amit Dhawan <amit.dhawan@barco.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-23 23:11:08 +00:00
Eduard van Valkenburg acb28a63b5 Python: Fix MCP metadata and tool name handling (#6656)
* Fix MCP metadata and tool name handling

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

* Address MCP review feedback

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 21:00:12 +00:00
Eduard van Valkenburg f2d02e58b3 Python: Add hosting core and Responses channel (#6580)
* Add Python hosting core and Responses channel

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

* Address hosting core review feedback

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

* Adopt source pyright typing setup for hosting packages

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

* Cover ResponsesChannel custom path routing

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

* Align hosting tests with package layout

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

* Fix hosting workflow fixture imports in aggregate tests

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

* Apply useful Responses channel hardening

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

* Fix hosting package typing checks

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

* Fix hosting pyright under Python 3.11

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

* Avoid static diskcache dependency in hosting

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

* Fix aggregate typing and Docker test resilience

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

* Simplify local Responses workflow sample

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

* Clarify generic hosting is not Foundry hosting

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

* Revert "Clarify generic hosting is not Foundry hosting"

This reverts commit 73b584d919053bed43a258d75dc2b76406e9c181.

* Clarify isolation key source flexibility

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

* Clarify isolation header reuse boundary

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

* Support multimodal Responses channel outputs

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

* Preserve multimodal streaming Responses output

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

* Stream Responses output items from updates

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

* Improve Responses streaming output handling

* Tighten Responses channel default option handling

- Restore full option parsing in parse_responses_request: known fields
  are remapped (max_output_tokens→max_tokens, parallel_tool_calls→
  allow_multiple_tool_calls), transport/session keys excluded, None
  values dropped, everything else forwarded as-is so run_hook can
  inspect the full set.
- Add a default _strip_options_hook on ResponsesChannel that removes
  all parsed options before reaching the agent. Callers cannot inject
  generation params (temperature, instructions, tools, …) unless the
  host explicitly allows it.
- A custom run_hook replaces the default entirely and receives the
  full ChannelRequest.options plus the raw protocol_request.
- Update tests to cover remap, default-strip, and custom-hook paths.
- Clarify host debug-log docstring to match new option flow.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 20:56:46 +00:00
Tao Chen 36420c515e Python: Align serialized tool format to OTel GenAI tool def format (#6556)
* Align serialized tool format to OTel GenAI tool def format

* Cache serialized tools
2026-06-23 20:47:18 +00:00
Peter Ibekwe 1109d0bf64 Get date suffix up to date with release date (#6690) 2026-06-23 18:58:19 +00:00
Peter Ibekwe e030fb53de .NET: Replace the symlink index entries with regular file entries (#6687)
* replace the symlink index entries  with regular file entries

* Fixed broken links.
2026-06-23 16:16:23 +00:00
SergeyMenshykh e6ebba1884 Add ADR 0029: Skills over MCP implementation design options (#6679)
* Add ADR 0029: Skills over MCP implementation design options

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

* Potential fix for pull request finding

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

---------

Co-authored-by: SergeyMenshykh <SergeMenshikh@outlook.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-23 15:51:05 +00:00
Tao Chen 7051a4920d Python: Add MCP as a hard dep in Foundry Hosting (#6634)
* Add MCP as a hard dep in Foundry Hosting

* Pin GitHub SDK

* Fix formatting

* Fix formatting
2026-06-23 15:07:05 +00:00
Roger Barreto 15df1152fc .NET: Add sample for per-run refreshable MCP authentication headers (#6624)
* Add sample for per-run refreshable MCP authentication headers

Adds a Foundry RAPI sample that attaches per-run, refreshable authentication headers to MCP requests using existing primitives: a DelegatingHandler on the MCP transport's HttpClient plus an AsyncLocal run scope. The same agent runs under two contexts, each minting a fresh token, proving the header is per run rather than bound at agent or connection creation time.

The handler attaches the bearer only over HTTPS to the MCP server's own origin, logs the non-secret label only, disables cookies, and checks certificate revocation. The README covers security considerations and production notes.

Fixes #1631

* Address PR review: harden redirect handling, nest-safe scope, README env vars

Disable AllowAutoRedirect on the shared handler so a redirect cannot carry the bearer past the origin check. Save and restore the prior run scope instead of clearing to null so the helper is safe under nesting. Note the Foundry env vars in the samples folder README row and update the sample README security notes.
2026-06-23 15:03:44 +00:00
westey a2018b40f9 Python: [BREAKING] Require approval for file-access tools with read-only auto-approval (#6599)
* Require approvals for file-access and expose auto approval funcs for it

* Scope file-access auto-approval rules to local tools; fix base-Agent sample

Address PR #6599 review feedback:
- read_only/all_tools auto-approval rules now reject any call carrying a
  server_label so they stay scoped to FileAccessProvider's local tools and
  never auto-approve a same-named hosted tool.
- Expand the FileAccessProvider docstring to explain the runtime effect of
  approval_mode="always_require" and point to ToolApprovalMiddleware /
  create_harness_agent.
- Fix the base-Agent file_access_data_processing sample, which would otherwise
  stop executing file tools under the new always_require defaults, by adding
  ToolApprovalMiddleware with all_tools_auto_approval_rule.
- Add tests covering hosted (server_label) calls and update docs.

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

* Clean up comments

* Update sample after merge

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 09:51:12 +00:00
SergeyMenshykh e4b89373f1 .NET: Explicitly emit available_resources and available_scripts in skill content (#6672)
* .NET: Explicitly emit available_resources and available_scripts in skill content

AgentInlineSkillContentBuilder now always emits <available_resources> and
<available_scripts> elements, using self-closing tags when a skill has no
resources or scripts. This signals to the model exactly what is callable so it
does not hallucinate non-existent resource or script names. Script parameter
schemas are wrapped in a nested <parameters_schema> element.

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

* .NET: Emit available_resources block for file-backed skills

Align AgentFileSkill with inline/class skills by surfacing discovered
resources in the loaded skill content. AgentFileSkill.GetContentAsync now
appends an <available_resources> block (before <available_scripts>) listing
resource names so the model has an authoritative list and does not
hallucinate resource names. Extracted a reusable BuildAvailableResourcesBlock
helper in AgentInlineSkillContentBuilder.

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

---------

Co-authored-by: SergeyMenshykh <SergeMenshikh@outlook.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 09:51:10 +00:00
SergeyMenshykh 88f0b23fb0 .NET: Change A2A default session store to NoopAgentSessionStore (#6635)
* Change A2A default session store to NoopAgentSessionStore

Align the A2A hosting layer default session store with the AG-UI
sibling by using NoopAgentSessionStore, making persistence an explicit
opt-in choice.

Update samples to document how to register a persistent session store
for multi-turn conversations.

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

* Clarify test name to specify session store default

Rename test to FallsBackToNoopSessionStoreDefaultAsync to avoid
implying all stores default to noop (task store still uses InMemory).

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 08:55:59 +00:00
Peter Ibekwe 9ba6b3a94e Remove unnecessary declarative logging (#6677) 2026-06-23 00:35:12 +00:00
Peter Ibekwe 2999f7416f Update package release version (#6673) 2026-06-22 22:13:50 +00:00
Roger Barreto 09791533cf .NET: Emit execute_tool spans by placing OpenTelemetry below FunctionInvokingChatClient (#6667)
OpenTelemetryAgent auto-wired OpenTelemetryChatClient above FICC, producing
OTel(FICC(leaf)). FICC resolved its ActivitySource at construction time as null,
so execute_tool spans were never emitted for tool-calling agents.

This repositions OTel below FICC, producing FICC(OTel(leaf)), via a deferred
NoOp slot pre-placed as the innermost decorator in WithDefaultAgentMiddleware
and activated once at the agent level.

- Add internal DeferredOpenTelemetryChatClient: inert DelegatingChatClient whose
  Activate(sourceName) swaps its target to inner.AsBuilder().UseOpenTelemetry().Build().
- WithDefaultAgentMiddleware always registers the slot innermost so it lands below FICC.
- OpenTelemetryAgent activates the slot once in its constructor and forwards run
  options straight through, removing the per-run ChatClientFactory outer wrap.
- Add and update unit tests, including a proof that execute_tool spans are emitted
  on the agent source and parented under invoke_agent.
2026-06-22 15:09:29 -07:00
Tao Chen 7f2e19ca2f Python: Ensure spans created inside sync preparations in streaming call are correctly nested (#6552)
* Make sure spans created inside sync ops in streaming path are correctly nested

* Add tests

* Fix comments

* Fix typing
2026-06-22 19:46:11 +00:00
westey 7b6f582b13 Python: Agent Harness blog post accompanying samples part 1 (#6605)
* Add samples for harness blog post part 1

* Add readme for python samples

* Update python instructions to match dotnet instructions

* Address PR comments

* Add link to blog posts

* Fix blog post naming.

* Add more blog post links
2026-06-22 18:33:36 +01:00
Giles Odigwe dc60722cee .NET: Project ToolExecution events as FunctionCallContent/FunctionResultContent in GitHubCopilotAgent streaming (#6228)
* Project ToolExecution events as FunctionCallContent/FunctionResultContent

GitHubCopilotAgent's event-dispatch switch previously had no case for
ToolExecutionStartEvent or ToolExecutionCompleteEvent. Both fell through
to the default case and were wrapped as opaque AIContent with
RawRepresentation, preventing downstream consumers and models from
recognizing tool call results.

Add explicit cases that project:
- ToolExecutionStartEvent → FunctionCallContent (role: Assistant)
- ToolExecutionCompleteEvent → FunctionResultContent (role: Tool)

This mirrors the Python fix already shipped in #4734/#4814/#4828.

Fixes #5897

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

* fix(#5897): Address review feedback for ParseArguments robustness

- Handle non-generic IDictionary variants (Hashtable, etc.) that don't
  match IDictionary<string, object?> due to generic invariance
- Return null for empty/whitespace string arguments instead of wrapping
  them in a spurious { value = "" } dictionary, aligning with
  ParseFunctionArgumentsObject convention elsewhere in the repo
- Add test coverage for Dictionary, Hashtable, and JsonElement argument
  types
- Add edge-case test for Success=true with null Result

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

* Fix non-generic IDictionary key handling in ParseArguments (#5897)

Use direct (string) cast for dictionary keys instead of ToString()
coercion, matching the established pattern in ObjectExtensions and
PortableValueExtensions. This validates keys are actually strings
rather than silently accepting and coercing non-string keys.

Add test verifying non-string dictionary keys throw InvalidCastException.

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

* Fix missing 'using System' in ToolExecutionEventProjectionTests

Add the missing 'using System' directive needed for InvalidCastException
reference at line 375 of ToolExecutionEventProjectionTests.cs.

Fixes #5897

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

* Use source-generated JsonTypeInfo for AOT-safe argument deserialization

Replace reflection-based JsonSerializer.Deserialize<T>() calls with the
JsonTypeInfo overload that uses source-generated metadata, eliminating
IL2026/IL3050 trimming and AOT warnings without suppressions.

Changes:
- Register Dictionary<string, object?> in GitHubCopilotJsonUtilities JsonContext
- Add JsonSerializerOptions constructor parameter (defaults to
  GitHubCopilotJsonUtilities.DefaultOptions)
- Use GetTypeInfo()-based Deserialize overload in ParseArguments
- Remove [UnconditionalSuppressMessage] attributes

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

* Fix dotnet format: add 'this.' qualification to instance method call

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

* Adapt to GitHub.Copilot.SDK 1.0.0 API after merge with main

- Update ToolExecutionEventProjectionTests: Arguments is now JsonElement?
  (not object?), remove tests for string/Dictionary/Hashtable arguments
- Remove AutoStart option (removed in 1.0.0)
- Simplify ParseArguments to handle JsonElement primarily
- Add tests for empty object and nested JSON arguments

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-06-22 16:45:03 +00:00
Peter Ibekwe 2f5a76ab1d Fix issue with resuming checkpoint after package version upgrade (#6636) 2026-06-22 15:18:22 +00:00
Eduard van Valkenburg a7381d8bef Python: stabilize dependency maintenance final checks (#6662)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-22 14:23:29 +00:00
westey d108d4b549 Python: [BREAKING] Integrate looping into HarnessAgent (#6607)
* Integrate looping into harness

* Address PR comments

* Address PR comments.

* Fix typing error
2026-06-22 13:14:30 +00:00
Eduard van Valkenburg fd160a7782 Python: fix dependency maintenance cutoff (#6658)
* Python: fix dependency maintenance cutoff

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

* Python: fix Hyperlight output dir typing

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-22 11:46:26 +00:00
Copilot ad3c1535c4 fix: propagate EnableSensitiveData to auto-wired inner OpenTelemetryChatClient (#6096)
When _autoWireChatClient=true and the caller sets EnableSensitiveData=true on
the outer OpenTelemetryAgent, the auto-wired inner OpenTelemetryChatClient now
also has EnableSensitiveData propagated, so the inner chat span correctly
captures message content (gen_ai.input.messages / gen_ai.output.messages).

Red test added first to reproduce the bug, then the fix applied (green).

Fixes #5873

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/fda69dd4-9576-4f3f-b954-514321652ea9

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
2026-06-22 10:27:13 +00:00
安妮的心动录 10b7d08bff .NET: fix(hosting): emit url_citation annotation events from streamed AI Search responses (#6649)
* fix(hosting): emit url_citation annotation events from streamed AI Search responses

OutputConverter.ConvertUpdatesToEventsAsync accumulated text content deltas but
silently dropped CitationAnnotation metadata from TextContent.Annotations. As a
result, hosted agents that use CreateAzureAISearchTool emitted citation markers in
text (e.g. 【5:0†source】) but produced empty annotations arrays and no
response.output_text.annotation.added SSE events.

The fix accumulates UrlCitationBody SDK annotations across all TextContent updates
for a message and emits them via TextContentBuilder.EmitAnnotationAdded after
EmitTextDone (as required by the SDK lifecycle) and before EmitDone. Non-citation
and region-less annotations are silently skipped, matching the existing OpenAI
ChatCompletions path in AgentResponseExtensions.

Adds 7 unit tests (N-01–N-07) covering: basic emission, ordering constraints,
multiple annotations, multi-update accumulation, and skip conditions.

Fixes #6641

* test: convert annotation test comments to XmlDoc and group in region

* fix: remove redundant long casts on annotation region indices

* test: assert done events carry url_citation annotation metadata

---------

Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
2026-06-22 10:10:02 +00:00
Eduard van Valkenburg fc3111c391 Python: Add FoundryAgent conversation session helper (#6623)
* Add FoundryAgent conversation session helper

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

* Simplify Foundry conversation session helper

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

* Rename Foundry conversation helper

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

* use named kw

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-22 08:41:15 +00:00
Roger Barreto 098e521586 .NET: Bring Hosted-Toolbox sample to parity with sibling hosting samples (#6633)
* Bring Hosted-Toolbox sample to parity with sibling hosting samples

Adds the standard scaffolding files (.env.example, agent.yaml, agent.manifest.yaml,
Dockerfile, Dockerfile.contributor) that every other 04-hosting Foundry sample ships
but Hosted-Toolbox lacked.

Fixes the toolbox name environment variable: reads TOOLBOX_NAME instead of the
platform reserved FOUNDRY_TOOLBOX_NAME so it survives agent create, and aligns the
default to my-toolset.

Rewrites the README to the standard section layout with PowerShell fenced commands,
and adds Using-Samples READMEs documenting why the client REPLs exist.

Renames Azure AI Foundry to Foundry across the 04-hosting sample READMEs and comments
for consistent product naming.

* Address PR review: accurate docs and TOOLBOX_NAME in ToolboxMcpSkills

- SimpleAgent README: correct the demo banner to the real per-agent URL the
  client prints (https scheme and the /api/projects/<project> segment).
- Hosted-Toolbox Program.cs: move FOUNDRY_MODEL out of the Required block into
  Optional since it has a gpt-4o default and an AZURE_AI_MODEL_DEPLOYMENT_NAME
  fallback.
- Hosted-ToolboxMcpSkills: switch the toolbox name from the reserved
  FOUNDRY_TOOLBOX_NAME to TOOLBOX_NAME across Program.cs, .env.example,
  agent.yaml, agent.manifest.yaml and README so it is deployable via the
  manifest, matching the other toolbox samples.
2026-06-20 09:31:19 +00:00
Eduard van Valkenburg 7435dd48d0 Python: harden Hyperlight output capture against symlinks (#6601)
* Python: harden Hyperlight output capture against symlinks

Mirror the input-staging symlink hardening on the output-capture path of
HyperlightExecuteCodeTool. Output discovery now walks via the symlink-safe
_iter_real_entries instead of rglob, per-file collection validates that no
path component is a symlink and the final entry is a regular file, and file
reads use os.O_NOFOLLOW. Adds regression tests for the output path.

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

* Address review: reject traversal, fix listing test, harden read

- _is_safe_output_file now rejects '.'/'..' components (lexical relative_to
  could otherwise escape root without a symlink)
- _read_output_file_bytes adds a cross-platform TOCTOU guard (lstat/fstat
  st_dev+st_ino identity check) since O_NOFOLLOW is absent on Windows
- fix intermediate-dir-symlink test to use a relative listing path so it
  exercises normalization + validation; add a parent-traversal unit test

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-19 22:43:38 +00:00
Ahmed Muhsin 148f57020a Python: host MAF workflows on a standalone Durable Task worker (#6418)
* feat(durabletask): host MAF workflows on a standalone Durable Task worker

Add a host-agnostic workflow execution engine to agent-framework-durabletask so a MAF Workflow can run as a durable orchestration outside Azure Functions:

- WorkflowOrchestrationContext protocol + DurableTaskWorkflowContext adapter, the superstep orchestrator, serialization helpers, capturing runner context, and the shared non-agent activity body (including the yield-output classifier so intermediate executors are not surfaced as final outputs).

- DurableAIAgentWorker.configure_workflow auto-registers agent executors as entities, non-agent executors as activities, and the workflow orchestrator.

- plan_workflow_registration centralizes the 'what to register' decision so it can be shared across hosts.

- run_agent_coroutine runs all agent coroutines on one persistent event loop, fixing a cross-loop hang when shared chat clients/credentials bind their asyncio primitives to a dead loop.

- DurableWorkflowClient (start/await workflow + HITL discover/respond); DurableAIAgentClient stays agent-only.

* refactor(azurefunctions): delegate workflow execution to agent-framework-durabletask

AgentFunctionApp now reuses the shared orchestrator, activity body, and registration planner from agent_framework_durabletask instead of maintaining its own copies; _workflow.py becomes a thin host-specific adapter (AzureFunctionsWorkflowContext).

- Run agent entity coroutines on the shared persistent event loop, fixing the cross-loop hang.

- Relocate state-diff unit tests to the durabletask package; update entity loop tests.

* feat(core): expose durabletask workflow symbols via agent_framework.azure

Lazily re-export WORKFLOW_ORCHESTRATOR_NAME and DurableWorkflowClient from the agent_framework.azure namespace so standalone hosts can import them without depending on internal module paths.

* docs(samples): add standalone durabletask workflow and HITL samples

Add two samples under samples/04-hosting/durabletask demonstrating MAF workflows on a standalone Durable Task worker (no Azure Functions):

- 08_workflow: conditional spam-detection workflow started via DurableWorkflowClient.start_workflow / await_workflow_output.

- 09_workflow_hitl: content-moderation workflow that pauses with ctx.request_info and is resumed via DurableWorkflowClient.get_pending_hitl_requests / send_hitl_response.

Also add the durabletask workflow integration test (test_08_dt_workflow).

* fix: address PR review feedback

- Sanitize HITL external-event responses with strip_pickle_markers in the orchestrator (defense-in-depth for callers that bypass DurableWorkflowClient).

- Raise WorkflowConvergenceException when max_iterations is reached with pending messages, matching the core WorkflowRunner instead of silently returning partial output.

- Route falsy 'sent' messages (use 'is not None' instead of truthiness).

- Normalize None shared_state_snapshot/source_executor_ids in execute_workflow_activity.

- Cast Any returns in AzureFunctionsWorkflowContext to satisfy mypy/pyright.

- Fix sample docstrings to reference DurableWorkflowClient.

* fix: resolve pyright Package Checks errors

- Use typed locals instead of cast in AzureFunctionsWorkflowContext (mypy sees Any, pyright sees concrete types -> avoid reportUnnecessaryCast).

- Annotate shared_state_snapshot and cast partially-typed durabletask SDK returns / HITL custom-status parsing to satisfy reportUnknownVariableType/reportUnknownMemberType.

- Drop the dead deserialize/serialize re-export in _workflow.py and mark the intentional private _extract_message_content re-export.

* fix(durabletask): agent-executor identity and typed workflow input

Register each workflow agent entity under the executor id that the orchestrator dispatches to (instead of the agent name), so AgentExecutor(agent, id=...) works when the id differs from agent.name. The azure-functions host mirrors this.

Reconstruct the start executor declared input type from the workflow initial JSON payload in the shared engine (mirroring in-process delivery) instead of string-coercing it per host. Untrusted input is stripped of pickle markers before reconstruction to prevent deserialization RCE.

* fix(samples): type durable workflow start executors for reconstructed input

The HITL and parallel workflow samples no longer hand-parse a JSON string. Their start executors now declare their real input type (ContentSubmission / DocumentInput), which the durable engine reconstructs from the client payload before delivery.

* test(durabletask): unit coverage for registration, client, worker, and input coercion

Add unit tests for plan_workflow_registration, DurableWorkflowClient, the agent-executor identity registration (entity keyed by executor id), and the typed initial-input coercion including pickle-marker neutralization.

* test(durabletask): HITL and parallel durable workflow integration tests

Add an integration test for the standalone durabletask HITL workflow sample via a new workflow_client fixture. Re-enable the Azure Functions parallel workflow test, consolidated into one end-to-end case so the work-stealing xdist scheduler cannot spawn multiple func hosts for this sample.

* refactor(durabletask): group workflow modules into a _workflows subpackage

Move the eight workflow modules into a private _workflows/ subpackage and drop the redundant _workflow_ prefix (orchestrator.py, registration.py, activity.py, client.py, context.py, dt_context.py, runner_context.py, serialization.py). The public API and __all__ are unchanged; only direct internal-module imports were repointed (package __init__, the worker, the azure-functions shared shim, and the affected unit tests).

* fix(durabletask): harden workflow type resolution and HITL response handling

- resolve_type returns only real classes (avoids issubclass TypeError in reconstruct_to_type)

- re-wait on HITL responses rejected by pickle-marker sanitization instead of dropping the request and losing the run

- American spelling in strip_pickle_markers docstring

- unit tests for resolve_type

* fix(durabletask): treat async edge conditions as not-matched on the synchronous host

The durabletask orchestrator evaluates edge conditions synchronously and does not support async edge conditions. Such an edge is now treated as not matched (the edge is not traversed) rather than assuming a result. Adds unit coverage; full async-condition support will be handled separately.

* fix(durabletask): reconstruct typed workflow outputs at the host boundary

await_workflow_output and the Azure Functions status endpoint now decode the checkpoint-encoded outputs the shared activity produces, via a shared deserialize_workflow_output helper. The client returns the original objects; the AF endpoint emits clean domain JSON instead of checkpoint-marker dicts, keeping the two hosts consistent.

* fix(durabletask): address review findings on workflow hosting

- AF: register workflow agents through add_agent(entity_id=...) so they remain tracked in app.agents / get_agent() (restores documented behavior) while keying by the executor id the orchestrator dispatches to; mirrors DurableAIAgentWorker.add_agent.

- async bridge: treat the shared loop as reusable only while its backing thread is alive, so a dead loop thread is replaced instead of hanging future.result() forever.

- client: add get_runtime_status; the standalone HITL sample now stops polling and reports the real terminal state instead of a generic timeout.

- tests: guard send_hitl_response pickle-marker stripping and add get_runtime_status coverage.

* fix(durabletask): wait indefinitely for HITL responses, matching core

The durable workflow host previously raced HITL responses against a 72h timer and failed the orchestration on elapse. MAF core's request_info has no timeout concept (it waits for the response), and the .NET durable host waits too, so the durable Python host now does the same: it stays paused until a response arrives. Removes the hitl_timeout_hours parameter and DEFAULT_HITL_TIMEOUT_HOURS constant from both hosts. A configurable timeout can be added later once core defines the contract (what happens on elapse).

* feat(durabletask): typed workflow event streaming and async client API

Add a brokerless workflow event stream to the durable host. Each non-agent executor runs inside a durable activity that captures its real WorkflowEvents (with data payloads); the orchestrator replays them into the orchestration custom status after each superstep, and the client streams them back as typed WorkflowEvent objects with reconstructed data. Agent executors contribute synthesized invoked/completed lifecycle events.

Add async client methods run_workflow (start with optional wait) and stream_workflow (typed event iterator), plus is_replaying plumbing through the orchestration context protocol and both host adapters so live status is published only on non-replay execution.

* docs(samples): standalone durabletask workflow streaming sample

Add sample 10_workflow_streaming demonstrating the async DurableWorkflowClient API on a standalone Durable Task worker: run_workflow(wait=False) to start without blocking, then stream_workflow to consume typed WorkflowEvent objects as a WriterAgent -> ReviewerAgent -> publish pipeline runs.

* refactor(durabletask): internal-only checkpoint codec and host-scoped workflow event streaming

Two related hardening changes to the durable workflow hosting layer, plus a
rebase-restored improvement.

Internal-only serialization codec (MSRC follow-up):
- Rename serialize_value/deserialize_value -> _serialize_value/_deserialize_value
  in the shared durabletask serialization module and update all call sites, so the
  pickle-backed checkpoint codec is unambiguously framework-internal. Untrusted
  input is still neutralized with strip_pickle_markers at the HTTP boundary.
- Remove the duplicate agent_framework_azurefunctions._serialization module and
  import strip_pickle_markers from the shared durabletask module instead. Move its
  unique serialization/strip-marker tests into the durabletask test suite.

Scope workflow event streaming to hosts that can carry it:
- Add WorkflowOrchestrationContext.supports_event_streaming. The standalone
  DurableTask host returns True (no custom-status size cap, has a stream_workflow
  consumer); the Azure Functions host returns False.
- The orchestrator now accumulates and publishes the WorkflowEvent timeline to the
  orchestration custom status only when the host supports streaming. On Azure
  Functions the custom status returns to its pre-streaming shape
  ({state[, pending_requests]}), which fixes orchestrator failures with
  "The size of the JSON-serialized payload must not exceed 16 KB" and stops leaking
  pickle markers into the HTTP status response. The Azure Functions status endpoint
  never consumed the event stream.

Workflow start endpoint:
- Accept text/plain raw request bodies (fall back from get_json to the raw body),
  restoring an improvement from main that the rebase conflict resolution dropped.

* fix(azurefunctions): scope workflow status/respond endpoints to the workflow orchestrator

The workflow/status/{instanceId} and workflow/respond/{instanceId}/{requestId}
HTTP endpoints resolved durable instances by ID only. The durable client looks up
IDs across every orchestration in the task hub (agent entities, any
user-registered orchestrations, and other apps sharing the hub), so a caller
holding one instance ID could read another orchestration's status -- including
pending HITL request payloads -- or inject external events into it.

Add AgentFunctionApp._is_workflow_orchestration() and gate both endpoints on it:
an instance whose orchestration name is not WORKFLOW_ORCHESTRATOR_NAME now returns
404 instead of leaking state or accepting events. send_hitl_response now fetches
the orchestration status and validates ownership before raising the external
event. Legitimate workflow instances are unaffected.

Mirrors the .NET fix in PR #6608.

* fix(durabletask): resolve CI typing failures

- serialization: rename _serialize_value/_deserialize_value back to
  serialize_value/deserialize_value to follow the package convention for
  cross-module internal helpers (matches strip_pickle_markers, resolve_type).
  The leading underscore tripped pyright reportPrivateUsage on cross-module
  imports under the strict source gate; internal-only status is preserved by
  not exporting them from the public API.
- Remove type-ignore comments pyright flags as unnecessary
  (reportUnnecessaryTypeIgnoreComment) in _worker.py, orchestrator.py,
  serialization.py.
- test_08_dt_workflow: add AgentClientFactoryProtocol and annotate the
  agent_client_factory fixture as type[AgentClientFactoryProtocol] (matching
  test_01-07) so mypy/ty stop reporting "type has no attribute create".
- samples (08_workflow, 09_workflow_hitl): pass structured output via
  FoundryChatOptions[Any](response_format=...) instead of a plain dict so the
  samples pyright (basic) config accepts default_options.

---------

Co-authored-by: Gavin Aguiar <80794152+gavin-aguiar@users.noreply.github.com>
2026-06-19 22:21:08 +00:00
Ben Thomas 89d19a2370 .NET: Migrate 01-get-started samples to Foundry as canonical default (#6555)
* Migrate 01-get-started samples to Foundry as canonical default

Change canonical provider from Azure OpenAI to Microsoft Foundry Responses API:

Code changes:
- Updated all 01-get-started samples (01_hello_agent, 02_add_tools, 03_multi_turn,
  04_memory, 06_host_your_agent) to use FoundryAgent or AIProjectClient.AsAIAgent()
- Updated environment variables: AZURE_OPENAI_* → FOUNDRY_PROJECT_ENDPOINT/FOUNDRY_MODEL
- Updated .csproj files to reference Microsoft.Agents.AI.Foundry instead of Azure.AI.OpenAI
- Added warning comments about DefaultAzureCredential production usage
- 05_first_workflow unchanged (workflow pattern only, no AI model)

Documentation changes:
- Updated AGENTS.md Default provider section to reflect Foundry as canonical
- Updated code example to use FoundryAgent constructor pattern
- Updated env var documentation

Note: 04_memory (AIContextProvider sample) extracts IChatClient from FoundryAgent
to maintain the memory pattern while using Foundry backend.

All samples verified to build successfully.

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

* Address PR 6555 review feedback and format failures

- Add Microsoft.Agents.AI.Foundry using to AGENTS.md Foundry snippet
- Update verify-samples GetStarted env vars to FOUNDRY_PROJECT_ENDPOINT/FOUNDRY_MODEL
- Remove unnecessary usings flagged by dotnet format in 01_get_started samples

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

* Switch 01-get-started samples from FoundryAgent to AIProjectClient.AsAIAgent()

Use AIProjectClient.AsAIAgent() as the canonical pattern for all 01-get-started
samples. Reserve FoundryAgent only for samples that specifically demonstrate the
Foundry-managed (prompt) agent — i.e. 02-agents/AgentsWithFoundry/.

Changes:
- 01_hello_agent, 02_add_tools, 03_multi_turn, 06_host_your_agent: swap
  FoundryAgent constructor for AIProjectClient.AsAIAgent(model, instructions)
- 04_memory: get IChatClient via AIProjectClient.AsAIAgent(options).GetService()
  instead of extracting from a throwaway FoundryAgent
- AGENTS.md: update default-provider snippet and note on when to use FoundryAgent

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-19 15:17:12 +00:00
Peter Ibekwe c815902344 .NET: InProcessRunnerContext bugfix for workflows (#6551)
* workfllow bugfix

* Update exception message

* Fix unit test.
2026-06-19 14:09:36 +00:00
Peter Ibekwe 074ac68a6c .NET: Harden fan-in barrier checkpoint state and extend resume coverage (#6574)
* Harden fan-in barrier checkpoint state and extend resume coverage

* Address PR comment
2026-06-19 13:33:26 +00:00
Eduard van Valkenburg d049d94b49 Python: consolidate dependency maintenance workflow (#6570)
* Python: consolidate dependency maintenance workflow

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

* Python: delay dependency maintenance updates

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

* Python: track dependency bounds test failures

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

* Python: scope dependency maintenance token

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-19 09:41:35 +00:00
dependabot[bot] 41995e265d Build(deps): Bump anthropic from 0.80.0 to 0.107.1 in /python (#6396)
Bumps [anthropic](https://github.com/anthropics/anthropic-sdk-python) from 0.80.0 to 0.107.1.
- [Release notes](https://github.com/anthropics/anthropic-sdk-python/releases)
- [Changelog](https://github.com/anthropics/anthropic-sdk-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/anthropics/anthropic-sdk-python/compare/v0.80.0...v0.107.1)

---
updated-dependencies:
- dependency-name: anthropic
  dependency-version: 0.107.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 08:53:17 +00:00
dependabot[bot] 2adacb3034 Bump aiohttp from 3.13.4 to 3.14.1 in /python (#6395)
---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.14.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 08:53:08 +00:00
dependabot[bot] 6e3836698b Bump Anthropic.Foundry from 0.5.0 to 0.6.0 (#6057)
---
updated-dependencies:
- dependency-name: Anthropic.Foundry
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 08:52:48 +00:00
dependabot[bot] 0d3e3504f0 Build(deps): Bump mistralai from 2.4.2 to 2.4.9 in /python (#6393)
Bumps [mistralai](https://github.com/mistralai/client-python) from 2.4.2 to 2.4.9.
- [Release notes](https://github.com/mistralai/client-python/releases)
- [Changelog](https://github.com/mistralai/client-python/blob/main/RELEASES.md)
- [Commits](https://github.com/mistralai/client-python/compare/v2.4.2...v2.4.9)

---
updated-dependencies:
- dependency-name: mistralai
  dependency-version: 2.4.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 08:45:29 +00:00
dependabot[bot] 2ba97ebcca Bump openai from 2.24.0 to 2.43.0 in /python (#6394)
Bumps [openai](https://github.com/openai/openai-python) from 2.24.0 to 2.43.0.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Changelog](https://github.com/openai/openai-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/openai/openai-python/compare/v2.24.0...v2.43.0)

---
updated-dependencies:
- dependency-name: openai
  dependency-version: 2.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 08:45:17 +00:00
hanhan761 2d0555c537 Python: re-role trailing assistant message to user for Anthropic compatibility (fixes #5008) (#6207)
* Fix auto function calling stripping explicit null arguments (fixes #5934)

* fix: re-role trailing assistant message to user for Anthropic (fixes #5008)

* fix: address Copilot review feedback (exclude_unset, test coverage, synthetic user turn)

* fix: update docstring and extend exclude_unset to auto_invoke_function

* revert: remove unrelated core _tools.py changes from Anthropic PR

The exclude_none/exclude_unset changes in the core package are out of scope
for this Anthropic-specific fix. This PR now only contains the Anthropic
chat client docstring fix and the synthetic user turn append.

* fix: avoid appending user turn after Anthropic tool use

* Fix Anthropic tool-use type narrowing

Use object-typed content narrowing before checking Anthropic tool-use block types so strict Pyright no longer treats dynamic message content as Unknown.

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

---------

Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-19 06:25:34 +00:00
Evan Mattson 5145d50be8 Python: Fix AG-UI tool history replay sanitization  (#6581)
* Python: Fix AG-UI tool history replay sanitization 

* Python: Address AG-UI replay review comments

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-19 15:01:01 +09:00
dependabot[bot] 7f7c88bfa5 Build(deps): Bump python-multipart from 0.0.26 to 0.0.32 in /python (#6406)
* Build(deps): Bump python-multipart from 0.0.26 to 0.0.27 in /python

Bumps [python-multipart](https://github.com/Kludex/python-multipart) from 0.0.26 to 0.0.27.
- [Release notes](https://github.com/Kludex/python-multipart/releases)
- [Changelog](https://github.com/Kludex/python-multipart/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Kludex/python-multipart/compare/0.0.26...0.0.27)

---
updated-dependencies:
- dependency-name: python-multipart
  dependency-version: 0.0.27
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Build(deps): Bump python-multipart to 0.0.32 via override-dependencies floor >=0.0.31

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-19 01:00:26 +00:00
Shyju Krishnankutty bcef77af6a .NET: (Durable): Scope workflow status/respond endpoints to route workflow name (#6608)
* Scope workflow status/respond endpoints to route workflow.

 Validate that the orchestration instance belongs to the workflow
 named in the route. Prevents cross-workflow access via runId.

* Add changelog.

* Address Copilot review feedback: fix duplicate XML doc, make IsOrchestrationOwnedByWorkflow non-throwing, drop misleading Async suffix in test name

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-19 00:01:15 +00:00
Ben Thomas 54a30571aa Dotnet - Add support for Foundry Adaptive evals (#6267)
* .NET: feat(evals): RubricScore type + EvalScoreResult.Dimensions

Adds the core rubric-evaluator surface that mirrors the Python work in

PR #6101 (commit e45b934cc). Provider-agnostic types only — no Foundry

coupling. Subsequent commits will wire these into FoundryEvals.

- RubricScore: per-dimension score record (Id, Score?, Applicable, Weight, Reason).

- EvalScoreResult.Dimensions: optional init-only list of RubricScore.

  Null for non-rubric (built-in) evaluators.

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

* .NET: feat(evals): GeneratedEvaluatorRef + assertion helpers

Adds the provider-agnostic surface for referencing a pre-existing rubric

evaluator and gating CI on per-item / per-dimension thresholds. Mirrors

Python PR #6101 commits e5830dd7f (ref type) and 4bc60462d (asserts).

- GeneratedEvaluatorRef: name + optional version/display-name, plus a

  Latest(name) factory for versionless refs (discouraged for CI; consumers

  should warn at run time).

- AgentEvaluationResults.AssertScoreAtLeast: walks DetailedItems[].Scores,

  optionally filtered by evaluator name, recurses into SubResults.

- AgentEvaluationResults.AssertDimensionScoreAtLeast: walks each score's

  Dimensions list, skips non-applicable dimensions by default, supports

  requireApplicable to flip that, recurses into SubResults.

- AgentEvaluationResults.AssertNoFailedItems: walks DetailedItems for

  fail/error statuses, recurses into SubResults.

All helpers throw InvalidOperationException (matches existing AssertAllPassed).

Truncates offender lists to the first 5 with a '+N more' suffix to keep

CI output readable, mirroring the Python helpers.

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

* .NET: feat(foundry-evals): accept GeneratedEvaluatorRef in evaluators=

Adds FoundryEvaluatorSpec, a readonly-struct union with implicit conversions
from both string and GeneratedEvaluatorRef so call sites can mix built-in
evaluator names with rubric evaluator references:

    var evals = new FoundryEvals(
        projectClient, model,
        new GeneratedEvaluatorRef("policy-rubric", "3"),
        FoundryEvals.Relevance,
        FoundryEvals.Coherence);

FoundryEvals constructors (3 overloads), EvaluateTracesAsync, and
EvaluateFoundryTargetAsync now take FoundryEvaluatorSpec[]/params instead of
string[]/params. Existing call sites using string literals or string[] keep
working unchanged via implicit conversion.

FoundryEvalConverter.BuildTestingCriteria emits the documented Foundry wire
format for rubric refs:
  {
    "type": "azure_ai_evaluator",
    "name": <DisplayName ?? Name>,
    "evaluator_name": <Name>,
    "evaluator_version": <Version>,   // omitted when null
    "initialization_parameters": { "deployment_name": <model> },
    "data_mapping": { conversation arrays, optional tool_definitions }
  }

WireTestingCriterion gains an optional EvaluatorVersion field. Rubric refs
are preserved through FilterToolEvaluators (tool-aware but not tool-required)
and ignored by FindMissingGroundTruthEvaluators. A versionless ref emits a
Trace.TraceWarning at criterion-build time so CI authors notice the floating
version (mirrors the Python warning).

Adds 6 new Foundry unit tests (3 BuildTestingCriteria rubric paths, 1
FindMissingGroundTruthEvaluators, 1 FilterToolEvaluators preservation, 1
mixed-order). 369/369 Foundry tests pass.

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

* .NET: feat(foundry-evals): parse rubric dimension_scores into RubricScore

Adds FoundryEvals.ParseRubricScores, called per result inside ParseDetailedItem.
Each EvalScoreResult now populates Dimensions when the evaluator's sample carries
a rubric breakdown.

Accepts three shapes for forward compatibility with provider SDK iterations:

  1. sample.properties.dimension_scores  (canonical Foundry runtime shape)
  2. sample.properties.rubric_scores     (preview/legacy key)
  3. top-level sample.dimension_scores / sample.rubric_scores  (defensive fallback)

Entries missing 'id', 'weight', or 'applicable' are skipped without invalidating
well-formed siblings. Non-applicable dimensions may omit 'score' (parsed as null).

Adds 6 unit tests covering canonical and legacy keys, top-level fallback, no-match
returns null, malformed-entry skipping, and the non-applicable null-score path.
375/375 Foundry tests pass.

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

* .NET: feat(samples): Evaluation_FoundryRubric end-to-end sample

Adds dotnet/samples/05-end-to-end/Evaluation/Evaluation_FoundryRubric mirroring
the Python evaluate_with_rubric_sample.py:

  - Fetches a pre-existing Foundry agent via AgentAdministrationClient
    (GetAgentAsync for latest, GetAgentVersionAsync when FOUNDRY_AGENT_VERSION
    is pinned).
  - References a rubric evaluator by GeneratedEvaluatorRef(name, version);
    falls back to GeneratedEvaluatorRef.Latest(name) with the documented
    floating-version warning.
  - Mixes the rubric with FoundryEvals.Relevance and FoundryEvals.Coherence
    in a single FoundryEvals run (implicit string-and-ref conversion).
  - Prints per-dimension breakdowns from EvalScoreResult.Dimensions for each
    item.
  - Demonstrates a CI quality gate with AssertDimensionScoreAtLeast("general_quality", 3.0).

Documents the FOUNDRY_PROJECT_ENDPOINT footgun (must be project-scoped URL
.../api/projects/<project>, not the bare Azure OpenAI endpoint) and the
Eval-Definition-vs-Rubric-Evaluator distinction in the README. Ships a
.env.example with the FOUNDRY_* variables.

Registers the project in agent-framework-dotnet.slnx and cross-links from
the sibling Evaluation_Multimodal / Evaluation_ExpectedOutputs READMEs.

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

* fix(foundry-evals): harden FoundryEvals public surface for review

Address PR #6267 review comments on the .NET FoundryEvals integration:

- Add source-compat overloads accepting `string[] evaluators` for `FoundryEvals` ctor, `EvaluateTracesAsync`, and `EvaluateFoundryTargetAsync` so existing callers passing string arrays keep compiling unchanged. New overloads forward via a private `ToSpecs` helper that wraps each name through the implicit `string -> FoundryEvaluatorSpec` conversion.

- Guard against `default(FoundryEvaluatorSpec)` entries (both `BuiltinName` and `GeneratedRef` null) that would NRE the downstream converter. Adds `FoundryEvaluatorSpec.IsValid` / `EnsureValid` plus an internal `EnsureAllSpecsValid` helper, wired into the main ctor and both static evaluation entry points.

- Add 6 unit tests covering the new validation surface.

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

* fix(sample): set ExitCode=1 when rubric dimension gate trips

PR #6267 review comment: the FoundryRubric sample swallowed the AssertDimensionScoreAtLeast failure, so a CI run that included it as a quality gate would still exit 0 even when the rubric regressed. Set `System.Environment.ExitCode = 1` in the catch so CI fails while still letting the rest of the sample's logging complete cleanly.

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

* fix(foundry-evals): search typed Sample directly for rubric scores

PR #6267 review comment: `_extract_rubric_scores` only searched the `properties` dict when the sample exposed one. When the Azure AI Projects typed SDK returns a Sample object that puts `dimension_scores` / `rubric_scores` directly on the instance (no `properties` wrapper), we missed them and surfaced no per-dimension scores.

Add an `else: containers.append(sample)` branch so non-dict typed samples are also inspected for the score keys. Covered by two new tests: one with `dimension_scores` directly on a typed Sample without a `properties` wrapper, and one with the legacy `rubric_scores` key in the same shape.

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

* test(evals): cover assert_score_at_least and assert_no_failed_items

PR #6267 review comments: both assertion helpers shipped without unit tests. Add `TestAssertScoreAtLeast` (above threshold, below w/ offenders, evaluator filter, sub_results recursion) and `TestAssertNoFailedItems` (all passing, failed/errored statuses, sub_results recursion) with a shared `_score_results` fixture builder.

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

* docs(samples): remove dead rubric-evaluator doc link from FoundryRubric sample

The Azure AI Foundry rubric evaluator concept doc page has not yet been published, so the link in the sample README and Program.cs comment 404s. Drop the references until the upstream doc is live.

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

* Potential fix for pull request finding

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

* Address PR 6267 review nits

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

---------

Co-authored-by: Ben Thomas <25218250+alliscode@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-18 20:50:28 +00:00
westey dc445592ed Python: [BREAKING] Port FileMemoryProvider and integrate FileMemoryProvider & FileAccess into the harness agent (#6547)
* Port FileMemoryProvider to python and integrate it and FileAccessProvider into the harness

* Address PR comments

* Address PR comments

* Create FileSystemAgentFileStore root lazily on first write

Construction no longer calls mkdir, so building a store (and therefore a
default create_harness_agent, which wires default file-memory and file-access
stores under the CWD) performs no filesystem writes and does not fail in
read-only working directories. The root directory is created on the first
write_file / create_directory call; all read/list/search operations already
tolerate a missing root. Updates docstrings and adds a regression test.

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

* Fix typing

* Fixing typing errors

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 20:15:46 +00:00
westey 92823e9e61 .NET: [BREAKING] Require approval for FileAccessProvider tools with auto-approval rules (#6521)
* Require approval for file-access

* Potential fix for pull request finding

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

* Rename DisableToolApproval to DisableToolAutoApproval for clarity

* Fix broken suggestion.

* Address PR comments and fix build issue.

* Update dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2026-06-18 18:55:22 +00:00
Eduard van Valkenburg 7a491f8e76 Python: Add hosting channel ADRs and spec (#6578)
* Add Python hosting channel ADRs

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

* Add Python hosting implementation spec

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 18:29:24 +00:00
Ben Thomas 015e3bcd3b .NET: Enabling sequential orchestration to pass entire conversation or only previous output. (#6554)
* Fix sequential workflow input forwarding

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

* Make sequential workflow context configurable

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

* Clarify sequential chain-only behavior

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

* Clarify sequential output messaging

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

---------

Co-authored-by: Ben Thomas <25218250+alliscode@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 17:53:47 +00:00
Eduard van Valkenburg 6e95517659 Python: Split type checkers by target (pyright source, 5 checkers on tests/samples) (#6443)
* Python: Split type checkers by target (pyright source, 5 checkers on tests/samples)

Rework the typing setup along the lines of the 'too many type checkers'
approach:

- Pyright (strict) is now the sole source-code type checker; mypy is
  removed from source and its [tool.mypy] block becomes a relaxed profile
  used only for tests/samples.
- Tests are checked by all five checkers (pyright relaxed, mypy, pyrefly,
  ty, zuban); samples by pyright, pyrefly, and ty. All run in a relaxed/
  basic profile so authors aren't forced into over-annotation.
- Add pyrightconfig.tests.json and bump sample pyright configs to basic.
- Unify test/sample typing onto the same parallel fan-out used by source
  pyright via run_command_items in task_runner.py.
- Make version-conditional imports symmetric: keep or drop the
  '# type: ignore' on both branches so results match across interpreter
  versions (local vs CI).
- Update SKILL.md, DEV_SETUP.md, and CODING_STANDARD.md for the five
  gating checkers and pyright on source+tests+samples.

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

* Python: Fix merge regressions from main (typing + runtime)

Merging main into the type-checker split branch surfaced regressions that
the new five-checker test suite and unit tests caught:

Runtime fixes:
- anthropic: restore the dropped `cache_read_input_token_count` mapping in
  _parse_usage_from_anthropic (lost during merge conflict resolution).
- gemini: _get_function_calling_mode test helper returned str(enum)
  ('FunctionCallingConfigMode.AUTO') instead of the enum value ('AUTO').
- openai: _response_id_from_token test helper was an infinite self-recursion;
  return token['response_id'].
- orchestrations: reset output_events per approval iteration so the terminal
  output assertion counts only the final run.
- core: drop a stale duplicate harness test whose message ('non-negative')
  contradicted the source ('positive').
- purview: import PolicyLocation/PolicyScope/ProtectionScopeActivities/
  ExecutionMode used by the processor tests.

Type-checker fixes (tests, relaxed profile):
- core: pyright/mypy/pyrefly/ty/zuban green-ups across the harness, MCP,
  observability and types tests.
- anthropic/openai: route provider-namespaced UsageDetails keys through a
  dict cast (extra_items TypedDict unsupported by mypy/ty).
- purview: typed model constructors and cache-mock casts.
- ag-ui: annotate WorkflowContext[Any, Any] so yield_output accepts test
  payloads, guard Optional forwarded_props, and ty-ignore intentional bad args.

Source pyright (sole source checker) flagged unnecessary ignores newly
introduced by merged code in core _tools.py and declarative _declarative_base.py.

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

* Python: Isolate per-package mypy cache in test-typing fan-out

The parallel test-typing fan-out runs many mypy processes concurrently,
all defaulting to a single shared ./.mypy_cache. Concurrent writes corrupt
the cache and mypy aborts with INTERNAL ERROR (intermittently, depending on
worker timing) -- which is why CI's Test Typing job failed on a shifting set
of packages while a single-package run was fine.

Give each mypy invocation an isolated cache dir keyed by its target paths so
incremental caching still works per package without races. Other checkers
(zuban/pyrefly/ty/pyright) maintain their own caches and are unaffected.

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

* Python: Make lab pyright-only on source (drop source mypy)

Lab was the last package still running mypy on its source code, requiring
mypy-only `# type: ignore` comments that pyright (the sole source checker
everywhere else) flags as unnecessary. Align lab with the rest of the
monorepo:

- Remove the lab source mypy poe tasks (mypy-gaia/lightning/tau2) and the
  now-dead strict [tool.mypy] config block.
- Drop the 'Run lab mypy' CI step; lab source is type-checked by pyright only.

Lab tests remain covered by the workspace test-typing fan-out (mypy, pyrefly,
ty, zuban, pyright over tests using the relaxed root config).

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

* Python: Fix test-typing regressions from latest main merge

A fresh merge from main brought in new test code never run under the
five-checker test-typing suite. Green up across the affected packages:

- core: narrow Optional span.attributes with 'and' guards in span filters
  and assert+cast the json.loads(...attributes[...]) reads (test_observability);
  match the existing as_agent ignore on the protocol-typed fixture (test_clients).
- openai: align new streaming tests with the established chat_options dict
  pattern (ChatOptions TypedDict isn't assignable to dict), route Optional
  .annotations[0] access through a small _first_annotation helper (mirrors the
  file's assert-not-None convention), and annotate a mapped ResponseStream.
- foundry_hosting: annotate error: dict[str, Any] = body.get(...) or {}
  (zuban needs the annotation).
- foundry: narrow ignores for the live AIProjectClient credential arg (pyrefly)
  and connections.get_default (zuban) SDK type gaps.

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

* updated pyright version

* pyright fix

* Python: Fix source typing for pyright 1.1.410

Pyright 1.1.410 tightened several checks. Apply the same source fixes as
upstream PR #6275:

- anthropic: import AsyncAnthropicBedrock from anthropic.lib.bedrock and
  AsyncAnthropicVertex from anthropic.lib.vertex (no longer re-exported from
  the anthropic top-level package -> reportPrivateImportUsage).
- core _types.py: cast the transform-hook result to UpdateT (reportAssignmentType).
- core _workflows/_events.py: annotate the @contextmanager helper as
  Generator[None] instead of Iterator[None] (reportDeprecated).
- redis: build the combined filter expression with an explicit loop instead of
  reduce(and_, ...), which pyright could no longer fully type (drops the now
  unused functools.reduce / operator.and_ imports).

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

* Python: Accept plain-text body in Azure Functions workflow/run endpoint

The workflow_orchestrator already accepts plain strings as well as JSON
objects via context.get_input(), but the start_workflow_orchestration HTTP
handler only accepted JSON and returned 400 for any non-JSON body. This made
the functions integration tests that POST text/plain to /api/workflow/run
(e.g. test_09_workflow_shared_state) fail consistently with 400 != 202.

Fall back to the raw request body (decoded as UTF-8) when the body is not
JSON, rejecting only a truly empty body. The JSON path is unchanged.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 15:06:20 +00:00
Evan Mattson 97bb1d588a Migrate to using issue type bug instead of label bug. (#6595) 2026-06-18 14:47:00 +00:00
Roger Barreto 1fc57c45ee .NET: Bump Azure.AI.Projects to 2.1.0-beta.3 (#6542)
* Bump Azure.AI.Projects to 2.1.0-beta.3

Updates Azure.AI.Projects from 2.1.0-beta.2 to 2.1.0-beta.3, together with the transitive Azure.Core (1.56.0 to 1.57.0) and System.ClientModel (1.12.0 to 1.13.0) pins that beta.3 requires (beta.3 forces System.ClientModel 1.13.0.0 via Azure.Core 1.57.0).

Migrates the affected samples and integration test to the beta.3 surface:
* MemorySearch sample: MemorySearchToolCallResponseItem renamed to MemorySearchToolCall, Results renamed to Memories, MemoryItem indirection removed.
* AgentSkills sample: skill provisioning/download API redesigned to a version based model (CreateSkillVersionFromFiles, GetSkillContent which now downloads and unzips), removing manual ZIP handling.
* Session files integration test: GetSessionFilesAsync now returns an async collection of SessionDirectoryEntry and renames the sessionId parameter to agentSessionId.

* Stream session file listing and short-circuit in integration test

Avoids materializing the entire session directory listing into a List. The test now streams GetSessionFilesAsync and breaks as soon as the expected entry is found, then asserts it was located.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 13:58:22 +00:00
Evan Mattson b3f8aaa9d7 Python: adjust coverage report handoff (#6576)
* Adjust coverage report handoff

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

* Simplify coverage report handoff check

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 12:35:45 +00:00
dependabot[bot] c22fc8d653 Build(deps): Bump esbuild, @tailwindcss/vite, @vitejs/plugin-react and vite (#6503)
Removes [esbuild](https://github.com/evanw/esbuild). It's no longer used after updating ancestor dependencies [esbuild](https://github.com/evanw/esbuild), [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite), [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). These dependencies need to be updated together.


Removes `esbuild`

Updates `@tailwindcss/vite` from 4.1.12 to 4.3.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/@tailwindcss-vite)

Updates `@vitejs/plugin-react` from 5.0.1 to 5.2.0
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/plugin-react@5.2.0/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.2.0/packages/plugin-react)

Updates `vite` from 7.3.2 to 8.0.16
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version:
  dependency-type: indirect
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.3.1
  dependency-type: direct:production
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.2.0
  dependency-type: direct:development
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 11:35:48 +00:00
dependabot[bot] a3131b8130 Build(deps): Bump esbuild, @vitejs/plugin-react and vite (#6501)
Removes [esbuild](https://github.com/evanw/esbuild). It's no longer used after updating ancestor dependencies [esbuild](https://github.com/evanw/esbuild), [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). These dependencies need to be updated together.


Removes `esbuild`

Updates `@vitejs/plugin-react` from 4.7.0 to 6.0.2
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.2/packages/plugin-react)

Updates `vite` from 7.3.2 to 8.0.16
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version:
  dependency-type: indirect
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.2
  dependency-type: direct:development
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 11:35:40 +00:00
dependabot[bot] 3d46595111 Python: Bump prek from 0.4.3 to 0.4.5 in /python (#6527)
* Bump prek from 0.4.3 to 0.4.5 in /python

Bumps [prek](https://github.com/j178/prek) from 0.4.3 to 0.4.5.
- [Release notes](https://github.com/j178/prek/releases)
- [Changelog](https://github.com/j178/prek/blob/master/CHANGELOG.md)
- [Commits](https://github.com/j178/prek/compare/v0.4.3...v0.4.5)

---
updated-dependencies:
- dependency-name: prek
  dependency-version: 0.4.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix python workspace prek pin mismatch

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-18 10:09:11 +00:00
dependabot[bot] 205f7bcca8 Python: Bump pytest from 9.0.3 to 9.1.0 across /python workspace (#6524)
* Bump pytest from 9.0.3 to 9.1.0 in /python

Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.0.3 to 9.1.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/9.0.3...9.1.0)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fix Python workspace pytest pin mismatch

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-18 10:08:10 +00:00
dependabot[bot] 2048289fb0 Build(deps): Bump pydantic-monty from 0.0.17 to 0.0.18 in /python (#6392)
Bumps [pydantic-monty](https://github.com/pydantic/monty) from 0.0.17 to 0.0.18.
- [Release notes](https://github.com/pydantic/monty/releases)
- [Commits](https://github.com/pydantic/monty/compare/v0.0.17...v0.0.18)

---
updated-dependencies:
- dependency-name: pydantic-monty
  dependency-version: 0.0.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 10:05:13 +00:00
westey 699916d639 Python: Add WebSearchDisplayObserver to harness console (#6572)
* Adding an observer to the python harness for web search tools

* Escape dynamic strings with rich.markup.escape() in WebSearchDisplayObserver

Apply rich.markup.escape() to all user/tool-provided strings (queries, URLs,
titles, patterns) before interpolation into Rich-markup-enabled output. This
prevents characters like '['/']' from being interpreted as Rich markup tags.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 09:42:30 +00:00
Roger Barreto 1ba5cd3f44 .NET: Scope argument-based standing approvals correctly in ToolApprovalAgent (#6486) (#6487)
Ensure an argument-scoped standing approval (the "always approve with exact
arguments" path) records an empty argument set rather than null when the
approved call has no arguments, so it matches only future no-argument calls.
null remains reserved exclusively for tool-level approvals, keeping the two
scopes distinct. This aligns the .NET behavior with the existing Python harness.

Adds regression tests covering the no-argument standing-approval flow, the
MatchesRule argument-scoping semantics, and empty-arguments rule serialization.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 09:13:24 +00:00
Roger Barreto 1519e50f2f Harden archive extraction guard so path containment is statically recognized (#6564) (#6565)
The Hosted-AgentSkills sample and its mirrored unit-test helper gated ZIP
extraction on `StartsWith(destinationRoot)` OR `Equals(destinationRoot)`. The
second branch left an acceptance path not covered by the containment check, so
static analysis could not prove the extraction sink stays within the
destination. Make the single resolved-path StartsWith check the only gate to
extraction in both files and add a nested-entry regression test.

Closes #6564
2026-06-18 09:12:00 +00:00
Evan Mattson b55992bb67 Bump Python package versions for 1.9.0 release (#6583)
Selective, CHANGELOG-driven version bumps for the 2026-06-18 release.

Released tier: agent-framework-core and the root agent-framework go to 1.9.0
(minor). Core ships new public APIs (agent-loop middleware, tool-approval
middleware and harness integration, shell-tool harness integration, AG-UI
thread snapshot persistence, context-provider telemetry) plus two behavioral
breaking changes on evolving surfaces: MCP sampling now denies server-initiated
requests by default, and the FileAccess tools were aligned with the .NET
implementation. These are treated as within-1.x changes because every package
caps core at <2; a major bump would require rewriting those caps. The foundry
and openai packages go to 1.8.2 (patch, bug fixes only). The root
agent-framework-core[all] pin was moved to 1.9.0 in lockstep with core.

Release-candidate tier: ag-ui to 1.0.0rc5 and declarative to 1.0.0rc2 for their
respective changes. orchestrations is promoted to stable 1.0.0; PACKAGE_STATUS
and the README install hint were updated accordingly.

Prerelease tier (new Pacific date stamp 260618): anthropic (beta),
azure-contentunderstanding (alpha) and foundry-hosting (alpha). No beta cohort
bump was applied; only packages with changes this cycle were stamped.

Dependency floors: following the established convention, the core floor was
raised to >=1.9.0 on every non-core package bumped this cycle, preserving the
existing <2 upper bound.

Also resolves two pre-existing failures in the dependency-bounds validator that
are unrelated to the version bumps. Hosted-environment detection now catches a
bare ImportError so optional Foundry hosting probing cannot crash user-agent
setup. The harness shell-tool integration, which lazily imports the separate
agent-framework-tools package to avoid a circular runtime dependency, is now
type-checked and tested in isolated environments via a core dev
dependency-group, with the shell-tool tests guarded to skip when that package
is absent.
2026-06-18 18:01:17 +09:00
Evan Mattson e8cec71ed8 Use issue type for triage workflow (#6577)
* Use issue type for triage workflow

* Disable blank issue reports

* Revert "Disable blank issue reports"

This reverts commit 222c8444a7b3b5768e01b9d562195a27d1a29f1a.
2026-06-18 14:48:00 +09:00
Eduard van Valkenburg d7e63d7d0e Fix Foundry aiohttp dependency (#6567)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 02:03:16 +00:00
Changjian Wang f59d5c67d8 Python: Adopt azure-ai-contentunderstanding to_llm_input in CU context provider (#5796)
* Refactor DocumentEntry model and update result handling

- Changed the type of `result` in DocumentEntry from dict to str to store LLM-ready text.
- Introduced `search_payload` in DocumentEntry for optional alternate rendering.
- Updated FileSearchConfig to include `include_fields` option for vector store uploads.
- Modified tests to reflect changes in DocumentEntry and FileSearchConfig.
- Adjusted integration tests to validate new result structure and rendering.
- Removed legacy format_result tests as rendering is now handled by the SDK.

* Potential fix for pull request finding

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

* Add test to ensure page markers are preserved in LLM input

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

* fix(cu-context-provider): scope LLMStats telemetry filter to rai_warnings block

Address PR #5796 review comment: the previous defensive scrubber ran a global regex substitution over the full rendered string, so any markdown body bullet shaped like '- LLMStats: ...' would also be silently deleted.

Add a _strip_rai_telemetry helper that confines the substitution to the front-matter rai_warnings: YAML sub-block, leaving the body verbatim. Cover the new behavior with three tests (scoped strip, body preservation, and no-op branches).

* Sync uv.lock with azure-ai-contentunderstanding>=1.2.0b1 dependency bump

* Python: Drop search_payload/include_fields, single to_llm_input rendering (CU context provider)

Address PR #5796 review: remove the redundant search_payload field and _render_search_payload helper, drop the include_fields opt-in (already covered by output_sections), rename _resolve_pending_tokens -> _resolve_pending_analysis, and have _upload_to_vector_store read entry['result'] directly.

* Python: Adopt SDK 1.2.0b2 LLMStats filtering, drop local workaround (CU context provider)

azure-ai-contentunderstanding 1.2.0b2 filters LLMStats telemetry from rai_warnings and emits InputPageNumber page markers in to_llm_input, so the provider's local defense is redundant.

- Bump dependency to azure-ai-contentunderstanding>=1.2.0b2 (re-lock uv.lock)

- Remove _strip_rai_telemetry and its two regexes; _render_for_llm now returns to_llm_input(...) directly

- Delete 4 workaround unit tests for the removed helper

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: changjian-wang <v-changjwang@microsoft.com>
Co-authored-by: aluneth <wangchangjian1130@163.com>
2026-06-18 01:57:41 +00:00
Shyju Krishnankutty 26a0a7e8be .NET: (Durable): bind MCP threadId to the current agent and guard cross-agent session dispatch (#6531)
* scope MCP threadId to the current agent

* Fix Async suffix on test methods and add CHANGELOG entries

- Rename three test methods to include Async suffix (IDE1006 fix)
- Add CHANGELOG entries for DurableTask and Hosting.AzureFunctions

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-17 21:56:53 +00:00
Yufeng He 616315339e .NET: fix fan-in checkpoint edge state (#6491) 2026-06-17 20:17:53 +00:00
Eduard van Valkenburg fcc5576b04 .NET: feat(dotnet): Add LocalCodeAct package for local Python execution (#6105)
* feat(dotnet): Add LocalCodeAct package scaffold

Create Microsoft.Agents.AI.LocalCodeAct package with:
- Project file with embedded Python resources
- ExecutionMode enum (Subprocess only)
- ProcessExecutionLimits record
- FileMount record and FileMountMode enum
- README.md documentation
- Embedded Python runner and validator scripts

This is the .NET equivalent of the Python agent-framework-local-codeact
package. Next: Implement process bridge and tool integration.

* feat(dotnet): Add embedded Python runner and validator

Copy Python runner and validator scripts from the Python implementation
as embedded resources for the .NET package.

* feat(dotnet): Add CodeValidator wrapper

Implement CodeValidator.cs that:
- Extracts embedded Python validator script to temp file
- Invokes Python validator with JSON request
- Passes custom allow/block lists
- Throws CodeValidationException on failures
- Cleans up temp files

Uses the embedded Resources/validator.py for AST validation.

* feat(dotnet): Add LocalExecuteCodeFunction

Implement LocalExecuteCodeFunction as AIFunction:
- Accepts Python executable path (required)
- Registers host tools for code to call
- Validates code via CodeValidator if custom lists provided
- Executes via ProcessBridge
- Converts result dict to ChatMessage list
- Builds dynamic description including available tools

Matches Python LocalExecuteCodeTool functionality.

* feat(dotnet): Add LocalCodeActProvider

Implement AIContextProvider that:
- Injects execute_code tool into context
- Adds CodeAct instructions
- Enforces single-provider-per-agent via StateKeys
- Wraps LocalExecuteCodeFunction lifecycle

Minimal provider implementation matching Python LocalCodeActProvider.

* feat(dotnet): Add tests and sample for LocalCodeAct

Add unit tests:
- LocalExecuteCodeFunctionTests (4 tests)
- ProcessExecutionLimitsTests (2 tests)
- FileMountTests (2 tests)

Add sample:
- LocalCodeAct/Program.cs - Demonstrates provider and function usage
- LocalCodeAct/README.md - Documentation and safety warnings

Tests verify basic construction, metadata, and disposal.
Sample shows provider creation, function setup, and configuration.

Note: Build requires .NET 10 SDK per global.json.

* feat(dotnet): Add LocalCodeAct sample project

Add sample demonstrating:
- LocalCodeActProvider creation and configuration
- LocalExecuteCodeFunction direct usage
- Execution modes and file mount configuration
- Safety warnings and prerequisites

Includes project file and README with security guidance.

* feat(dotnet): Add file mount support and integration tests

- Added FileMountHelper.cs for file mount normalization, snapshot, and capture
- Updated LocalExecuteCodeFunction to support file mounts parameter
- Added file snapshot before/after execution with capture logic
- Updated LocalCodeActProvider to pass file mounts through
- Created comprehensive IntegrationTests.cs with 10 test cases:
  - Simple code execution
  - Timeout handling
  - Syntax error handling
  - Blocked import validation
  - Blocked builtin validation
  - Custom allowed imports
  - File mount read/write with capture
  - Stdout capture
  - Provider tool injection

All features from Python implementation now ported to .NET.

* Rewrite .NET LocalCodeAct to address all PR review comments

Complete rewrite that follows the Hyperlight package conventions
(see Microsoft.Agents.AI.Hyperlight) and addresses all 24 review
comments on PR #6105:

Architectural fixes:
* LocalCodeActProvider now uses options-class constructor pattern
  matching HyperlightCodeActProvider.
* Override of ProvideAIContextAsync uses the correct
  (InvokingContext, CancellationToken) signature returning
  ValueTask<AIContext>.
* ExecuteCodeFunction follows the AIFunction Name/Description/JsonSchema
  property pattern with InvokeCoreAsync override.
* Provider exposes AddTools/GetTools/RemoveTools/ClearTools and
  AddFileMounts/GetFileMounts/RemoveFileMounts/ClearFileMounts CRUD
  methods, with snapshot-at-invocation semantics under a lock.

Runtime/security fixes:
* Subprocess IPC uses JsonObject/JsonNode end-to-end (no
  Dictionary<string, object?> casts that broke under JsonElement
  deserialization).
* Validator runs in its own subprocess with a dedicated timeout
  (ProcessExecutionLimits.ValidationTimeoutSeconds), never reuses
  the runner script.
* Validation enabled by default; can be opt-ed out via
  ValidationEnabled = false.
* validator.py has a __main__ entrypoint that reads JSON from
  stdin and exits with structured errors.
* validator.py is now compatible with Python 3.9+ (Match nodes
  added conditionally).
* call_id parsed as long to match Python id(kwargs) range.

Other:
* README rewritten with valid C# syntax (options-class, FileMount
  constructor) and accurate descriptions of validator and file
  capture behavior.
* Added integration tests that exercise the real subprocess and
  validator (skipped gracefully when python3 is not on PATH).
* All 18 tests pass (15 unit + 3 integration) across net8/net9/net10.

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

* Sync embedded validator.py with Python package allow-list enforcement

The embedded Python validator script used by the .NET LocalCodeAct
package now enforces the builtin allow-list, matching the latest
behavior of agent_framework_local_codeact._validator. Names that are
real Python builtins must appear in the allow-list, while unknown names
(user-defined functions, registered tools) remain allowed.

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

* Add Hosted-LocalCodeAct foundry hosted-agent sample

Mirrors the Python foundry_hosted_agent.py sample for the local-codeact
package: registers compute and fetch_data as sandbox-only host tools on
LocalCodeActProvider so the model only sees execute_code and reaches them
via await call_tool(...). Includes the standard hosted-agent supporting
files (agent.yaml, agent.manifest.yaml, Dockerfile, Dockerfile.contributor,
.env.example, README.md) and installs python3 in the container images so
the embedded runner and validator can execute.

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

* fix(local-codeact-dotnet): sync validator os.* allow-list with Python

Mirror the Python package change: the embedded validator.py invoked by the
.NET ProcessBridge replaces the os.* deny-list with an allow-list of
{environ, path}. Add allowed_os_attrs parameter to validate_code and
_CodeValidator, and surface it via the stdin JSON request schema so the
.NET host can opt in to a broader allow-list when needed.

Default behavior tightens to match the documented contract: any os.*
attribute outside {environ, path} (for example os.listdir, os.open,
os.getcwd) is rejected.

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

* fix(local-codeact-dotnet): address review + tighten validator

- validator.py: enforce os.* allow-list on `from os import X` so names like
  `system`, `getcwd` cannot bypass the visit_Attribute restriction.
- ProcessBridge.ConfigureEnvironment: document that null Environment inherits
  the parent env (matching real behavior) and update the public
  LocalCodeActProviderOptions.Environment doc to describe the explicit
  empty-dictionary opt-in for a scrubbed environment.
- Tests:
  * FileMountHelperTests covers per-file, per-mount, and total
    capture-limit branches that return TextContent omissions.
  * Integration tests cover unknown-tool dispatch error, tool throwing
    exception, and CodeValidator timeout that kills the process and
    raises CodeValidationException.
- Sample: drop unused `Microsoft.Agents.AI.Foundry` using in
  Hosted-LocalCodeAct/Program.cs to satisfy IDE0005 check-format.

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

* chore(local-codeact-dotnet): remove stale orphan sample

The dotnet/samples/LocalCodeAct/ scaffolding sample referenced APIs
that don't exist in the current package (`ExecutionMode`, FileMount
object-initializer syntax, the old LocalExecuteCodeFunction
constructor signature, function.Metadata.*), produced a long list of
check-format violations (CHARSET, IMPORTS, IDE0073 header, IDE0005
unused using, IDE1006 Async suffix, RCS1037 trailing whitespace), and
did not match any of the documented sample layouts.

The hosted-agent example at
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct
is the supported entry-point sample for this package.

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

* style(local-codeact-dotnet): satisfy check-format rules

- Add UTF-8 BOM to source files (CHARSET)
- Remove unused using directives (IDE0005)
- Simplify type names (IDE0001/IDE0002/IDE0090)
- Rename static field JsonOptions -> s_jsonOptions (IDE1006)
- Rename static field SyncRoot -> s_syncRoot (IDE1006)
- Add missing this. qualifications in ProcessBridge (IDE0009)
- Remove unused _options field from LocalCodeActProvider (IDE0052)

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

* fix(local-codeact-dotnet): wire hosted sample into solution

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

* fix(local-codeact-dotnet): sync embedded Python scripts

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

* fix(local-codeact-dotnet): exercise Python integration on Windows

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

* .NET: Address LocalCodeAct API review feedback

Move the required Python executable path to LocalCodeAct constructors, invert the validation flag default, and apply small project/file mount cleanup suggestions.

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

* .NET: Address LocalCodeAct concurrency review

Surface unauthorized mount traversal errors and use concurrent provider registries for LocalCodeAct tool and file mount CRUD operations.

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

* .NET: Simplify LocalCodeAct function wrappers

Use AIFunctionFactory-created inner functions for LocalCodeAct execute_code wrappers and remove redundant script cache and JsonNode cloning logic.

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

* .NET: Update LocalCodeAct factory result tests

Handle JsonElement result values produced by AIFunctionFactory delegation in LocalCodeAct execute_code integration tests.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-17 15:30:52 +00:00
westey 6cc7ddb73e .NET: Integrate LoopAgent into HarnessAgent with TodoCompletionLoopEvaluator (#6544)
* Add LoopAgent to Harness with TodoEvaluator sample

* Address PR comments

* Fix build error
2026-06-17 19:03:16 +01:00
westey 39f4b5ec72 Align function tool names for BackgroundAgent and FileMemory between python and .net (#6550) 2026-06-17 17:14:12 +01:00
923 changed files with 50086 additions and 15770 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
name: .NET Bug Report
description: Report a bug in the Agent Framework .NET SDK
title: ".NET: [Bug]: "
labels: ["bug", ".NET"]
labels: [".NET"]
type: bug
body:
- type: textarea
+1 -1
View File
@@ -1,7 +1,7 @@
name: Python Bug Report
description: Report a bug in the Agent Framework Python SDK
title: "Python: [Bug]: "
labels: ["bug", "Python"]
labels: ["Python"]
type: bug
body:
- type: textarea
+5 -3
View File
@@ -24,12 +24,14 @@ updates:
- ".NET"
- "dependencies"
# Maintain dependencies for python
# Maintain dependencies for python.
# TODO: Remove these Python Dependabot entries after we have confidence in the
# Python dependency-maintenance workflow.
- package-ecosystem: "pip"
directory: "python/"
schedule:
interval: "weekly"
day: "monday"
day: "thursday"
labels:
- "python"
- "dependencies"
@@ -37,7 +39,7 @@ updates:
directory: "python/"
schedule:
interval: "weekly"
day: "monday"
day: "thursday"
labels:
- "python"
- "dependencies"
@@ -109,6 +109,9 @@ jobs:
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_MODEL_DEPLOYMENT_NAME }}
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
# Foundry
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
- name: Write Job Summary
if: always()
+3 -5
View File
@@ -2,7 +2,7 @@ name: Issue Triage
on:
issues:
types: [opened, labeled]
types: [opened, typed]
permissions:
contents: read
@@ -12,9 +12,7 @@ permissions:
concurrency:
group: >-
issue-triage-${{ github.repository }}-${{
((github.event.action == 'opened' && contains(github.event.issue.labels.*.name, 'bug'))
|| (github.event.action == 'labeled' && github.event.label.name == 'bug'))
&& github.event.issue.number
github.event.issue.type.name == 'Bug' && github.event.issue.number
|| github.run_id
}}
cancel-in-progress: true
@@ -28,7 +26,7 @@ env:
jobs:
team_check:
runs-on: ubuntu-latest
if: ${{ (github.event.action == 'opened' && contains(github.event.issue.labels.*.name, 'bug')) || (github.event.action == 'labeled' && github.event.label.name == 'bug') }}
if: ${{ github.event.issue.type.name == 'Bug' }}
outputs:
is_team_member: ${{ steps.check.outputs.is_team_member }}
issue_number: ${{ steps.issue.outputs.issue_number }}
+1 -3
View File
@@ -90,9 +90,7 @@ jobs:
// Check for issue type from issue form dropdown
const issueTypeField = getFormFieldValue(body, 'Type of Issue')
if (issueTypeField) {
if (issueTypeField === 'Bug') {
labels.push("bug")
} else if (issueTypeField === 'Feature Request') {
if (issueTypeField === 'Feature Request') {
labels.push("enhancement")
} else if (issueTypeField === 'Question') {
labels.push("question")
+4 -6
View File
@@ -113,8 +113,8 @@ jobs:
- name: Run markdown code lint
run: uv run poe markdown-code-lint
mypy:
name: Mypy Checks
test-typing:
name: Test Typing Checks
if: "!cancelled()"
strategy:
fail-fast: false
@@ -139,7 +139,5 @@ jobs:
os: ${{ runner.os }}
env:
UV_CACHE_DIR: /tmp/.uv-cache
- name: Run Mypy
env:
GITHUB_BASE_REF: ${{ github.event.pull_request.base.ref || github.base_ref || 'main' }}
run: uv run python scripts/workspace_poe_tasks.py ci-mypy
- name: Run tests/samples type checkers (mypy, pyrefly, ty)
run: uv run python scripts/workspace_poe_tasks.py ci-test-typing
@@ -0,0 +1,431 @@
name: Python - Dependency Maintenance
on:
workflow_dispatch:
schedule:
- cron: "0 4 * * 1"
permissions:
contents: write
issues: write
concurrency:
group: python-dependency-maintenance
cancel-in-progress: false
env:
UV_CACHE_DIR: /tmp/.uv-cache
jobs:
dependency-maintenance:
name: Dependency Maintenance
runs-on: ubuntu-latest
env:
# Match the existing Python dependency maintenance workflows. Reevaluate if package
# installability starts differing across supported Python versions.
UV_PYTHON: "3.13"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Set up python and install the project
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
env:
UV_CACHE_DIR: /tmp/.uv-cache
- name: Set dependency release cutoff
run: |
cutoff="$(date -u -d '7 days ago' '+%Y-%m-%dT%H:%M:%SZ')"
echo "DEPENDENCY_RELEASE_CUTOFF=${cutoff}" >> "$GITHUB_ENV"
echo "Using dependency release cutoff: ${cutoff}"
- name: Repin dev dependency declarations
run: uv run poe upgrade-dev-dependency-pins
working-directory: ./python
- name: Refresh lockfile after dev pin updates
run: uv lock
working-directory: ./python
- name: Save dev dependency changes
run: |
DEV_PATCH="${RUNNER_TEMP}/python-dev-dependency-updates.patch"
git diff -- python/pyproject.toml "python/packages/*/pyproject.toml" python/uv.lock > "${DEV_PATCH}"
if [ -s "${DEV_PATCH}" ]; then
echo "has_dev_changes=true" >> "$GITHUB_OUTPUT"
else
echo "has_dev_changes=false" >> "$GITHUB_OUTPUT"
fi
echo "patch=${DEV_PATCH}" >> "$GITHUB_OUTPUT"
id: dev_changes
- name: Run dependency bounds test scenarios
id: validate_bounds_test
continue-on-error: true
run: uv run poe validate-dependency-bounds-test --package "*"
working-directory: ./python
- name: Run dependency upper-bound validation
id: validate_ranges
if: steps.validate_bounds_test.outcome == 'success'
continue-on-error: true
run: uv run poe validate-dependency-bounds-project --mode upper --package "*"
working-directory: ./python
- name: Upload dependency validation reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: dependency-maintenance-results
path: |
python/scripts/dependencies/dependency-bounds-test-results.json
python/scripts/dependencies/dependency-range-results.json
if-no-files-found: warn
- name: Create issue for failed dependency bounds test
if: steps.validate_bounds_test.outcome != 'success'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require("fs")
const reportPath = "python/scripts/dependencies/dependency-bounds-test-results.json"
const owner = context.repo.owner
const repo = context.repo.repo
const openIssues = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: "open",
per_page: 100,
})
const openIssueTitles = new Set(
openIssues.filter((issue) => !issue.pull_request).map((issue) => issue.title)
)
const formatError = (message) => String(message || "No error output captured.").replace(/```/g, "'''")
const title = "Dependency bounds test failed"
if (openIssueTitles.has(title)) {
core.info(`Issue already exists: ${title}`)
return
}
const bodyLines = [
"Automated dependency bounds test mode failed before dependency upper-bound validation could run.",
"",
"The weekly dependency maintenance workflow kept only dev dependency updates for the generated PR, if any, and skipped dependency range updates for this run.",
"",
]
if (fs.existsSync(reportPath)) {
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"))
const failedScenarios = (report.scenarios ?? []).filter((scenario) => scenario.status === "failed")
for (const scenario of failedScenarios) {
bodyLines.push(`### ${scenario.name} scenario (${scenario.resolution})`)
const failedPackages = (scenario.packages ?? []).filter((pkg) => pkg.status === "failed")
for (const pkg of failedPackages.slice(0, 10)) {
bodyLines.push(
"",
`- Package: \`${pkg.package_name}\``,
`- Project path: \`${pkg.project_path}\``,
"",
"```",
formatError(pkg.error).slice(0, 3500),
"```"
)
}
if (failedPackages.length > 10) {
bodyLines.push("", `_Additional failed packages omitted: ${failedPackages.length - 10}_`)
}
}
} else {
bodyLines.push(`No dependency bounds test report was found at \`${reportPath}\`.`)
}
bodyLines.push("", `Workflow run: ${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`)
await github.rest.issues.create({
owner,
repo,
title,
body: bodyLines.join("\n"),
})
core.info(`Created issue: ${title}`)
- name: Create issues for failed dependency candidates
if: always()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require("fs")
const reportPath = "python/scripts/dependencies/dependency-range-results.json"
if (!fs.existsSync(reportPath)) {
core.info(`No dependency range report found at ${reportPath}`)
return
}
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"))
const dependencyFailures = []
for (const packageResult of report.packages ?? []) {
for (const dependency of packageResult.dependencies ?? []) {
const candidateVersions = new Set(dependency.candidate_versions ?? [])
const failedAttempts = (dependency.attempts ?? []).filter(
(attempt) => attempt.status === "failed" && candidateVersions.has(attempt.trial_upper)
)
if (!failedAttempts.length) {
continue
}
const failuresByVersion = new Map()
for (const attempt of failedAttempts) {
const version = attempt.trial_upper || "unknown"
if (!failuresByVersion.has(version)) {
failuresByVersion.set(version, attempt.error || "No error output captured.")
}
}
dependencyFailures.push({
packageName: packageResult.package_name,
projectPath: packageResult.project_path,
dependencyName: dependency.name,
originalRequirements: dependency.original_requirements ?? [],
finalRequirements: dependency.final_requirements ?? [],
failedVersions: [...failuresByVersion.entries()].map(([version, error]) => ({ version, error })),
})
}
}
if (!dependencyFailures.length) {
core.info("No failing dependency candidates found.")
return
}
const owner = context.repo.owner
const repo = context.repo.repo
const openIssues = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: "open",
per_page: 100,
})
const openIssueTitles = new Set(
openIssues.filter((issue) => !issue.pull_request).map((issue) => issue.title)
)
const formatError = (message) => String(message || "No error output captured.").replace(/```/g, "'''")
for (const failure of dependencyFailures) {
const title = `Dependency validation failed: ${failure.dependencyName} (${failure.packageName})`
if (openIssueTitles.has(title)) {
core.info(`Issue already exists: ${title}`)
continue
}
const visibleFailures = failure.failedVersions.slice(0, 5)
const omittedCount = failure.failedVersions.length - visibleFailures.length
const failureDetails = visibleFailures
.map(
(entry) =>
`- \`${entry.version}\`\n\n\`\`\`\n${formatError(entry.error).slice(0, 3500)}\n\`\`\``
)
.join("\n\n")
const body = [
"Automated dependency range validation found candidate versions that failed checks.",
"",
`- Package: \`${failure.packageName}\``,
`- Project path: \`${failure.projectPath}\``,
`- Dependency: \`${failure.dependencyName}\``,
`- Original requirements: ${
failure.originalRequirements.length
? failure.originalRequirements.map((value) => `\`${value}\``).join(", ")
: "_none_"
}`,
`- Final requirements after run: ${
failure.finalRequirements.length
? failure.finalRequirements.map((value) => `\`${value}\``).join(", ")
: "_none_"
}`,
"",
"### Failed versions and errors",
failureDetails,
omittedCount > 0 ? `\n_Additional failed versions omitted: ${omittedCount}_` : "",
"",
`Workflow run: ${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`,
].join("\n")
await github.rest.issues.create({
owner,
repo,
title,
body,
})
openIssueTitles.add(title)
core.info(`Created issue: ${title}`)
}
- name: Keep only dev updates when range validation fails
if: steps.validate_bounds_test.outcome != 'success' || steps.validate_ranges.outcome != 'success'
env:
DEV_PATCH: ${{ steps.dev_changes.outputs.patch }}
HAS_DEV_CHANGES: ${{ steps.dev_changes.outputs.has_dev_changes }}
run: |
git restore python/pyproject.toml python/packages/*/pyproject.toml python/uv.lock
if [ "${HAS_DEV_CHANGES}" = "true" ]; then
git apply "${DEV_PATCH}"
fi
- name: Refresh lockfile after dependency range updates
if: steps.validate_bounds_test.outcome == 'success' && steps.validate_ranges.outcome == 'success'
run: uv lock
working-directory: ./python
- name: Install final dependency set
run: uv run poe install
working-directory: ./python
- name: Run final checks
run: uv run poe check
working-directory: ./python
- name: Run final typing
run: uv run poe typing
working-directory: ./python
- name: Commit and push dependency updates
id: commit_updates
run: |
BRANCH="automation/python-dependency-maintenance"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -B "${BRANCH}"
git add python/pyproject.toml python/packages/*/pyproject.toml python/uv.lock
if git diff --cached --quiet; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No dependency updates to commit."
exit 0
fi
git commit -m "Python: chore: update dependencies"
git push --force-with-lease --set-upstream origin "${BRANCH}"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
- name: Create or update dependency maintenance tracking issue
if: steps.commit_updates.outputs.has_changes == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const branch = "automation/python-dependency-maintenance"
const prTitle = "Python: chore: update dependencies"
const issueTitle = "Python dependency maintenance PR needed"
const owner = context.repo.owner
const repo = context.repo.repo
const branchRef = await github.rest.git.getRef({
owner,
repo,
ref: `heads/${branch}`,
})
const branchSha = branchRef.data.object.sha
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`
const compareUrl = `${context.serverUrl}/${owner}/${repo}/compare/main...${branch}`
const prBody = [
"### Motivation & Context",
"",
"This automated update keeps Python dependency metadata coherent across the uv workspace. Python dependencies can be declared in multiple `pyproject.toml` files, but the workspace has one shared `python/uv.lock`, so dependency maintenance should update and validate them together instead of through per-manifest Dependabot PRs.",
"",
"### Description & Review Guide",
"",
"- **What are the major changes?** Refresh Python dev dependency pins, update package dependency ranges when the bounds tooling succeeds, and refresh `python/uv.lock`.",
"- **What is the impact of these changes?** Keeps the Python workspace dependency set current while producing at most one dependency PR for the week. If dependency range validation fails, this PR contains only the dev dependency updates that still pass final validation, and separate issues track failed range candidates.",
"- **What do you want reviewers to focus on?** Review the generated dependency metadata changes and any dependency-range updates for package-specific compatibility concerns.",
'<!-- NOTE FOR AUTOMATED/AI REVIEWERS: the "What do you want reviewers to focus on?"',
" item above is intended for human reviewers only. Automated/AI reviewers should",
" ignore it and review the entire change rather than narrowing scope to it. -->",
"",
"",
"### Related Issue",
"",
"No linked issue; this PR is generated by scheduled Python dependency maintenance.",
"",
"### Contribution Checklist",
"",
"- [x] The code builds clean without any errors or warnings",
"- [x] All unit tests pass, and I have added new tests where possible",
"- [x] The PR follows the [Contribution Guidelines](https://github.com/microsoft/agent-framework/blob/main/CONTRIBUTING.md)",
"- [ ] This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).",
'- [x] **This is not a breaking change.** If it _is_ a breaking change, add the `breaking change` label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.',
].join("\n")
const prBodyFence = "```"
const command = [
"PR_BODY_FILE=\"$(mktemp)\"",
`cat > "$PR_BODY_FILE" <<'EOF'`,
prBody,
"EOF",
"gh pr create --repo microsoft/agent-framework --base main \\",
` --head ${owner}:${branch} \\`,
` --title "${prTitle}" \\`,
" --body-file \"$PR_BODY_FILE\"",
].join("\n")
const issueBody = [
"The Python dependency maintenance workflow generated and validated dependency updates, then pushed them to the automation branch.",
"",
`- Branch: \`${branch}\``,
`- Commit: \`${branchSha}\``,
`- Compare: ${compareUrl}`,
`- Workflow run: ${runUrl}`,
"",
"GitHub Actions is not permitted to create pull requests in this repository, so a maintainer needs to create the PR manually.",
"",
"### Create the PR",
"",
"```bash",
command,
"```",
"",
"### Generated PR body",
"",
prBodyFence,
prBody,
prBodyFence,
].join("\n")
const openIssues = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: "open",
per_page: 100,
})
const existingIssue = openIssues.find((issue) => !issue.pull_request && issue.title === issueTitle)
if (existingIssue) {
await github.rest.issues.update({
owner,
repo,
issue_number: existingIssue.number,
title: issueTitle,
body: issueBody,
})
core.info(`Updated issue #${existingIssue.number}: ${issueTitle}`)
} else {
const createdIssue = await github.rest.issues.create({
owner,
repo,
title: issueTitle,
body: issueBody,
})
core.info(`Created issue #${createdIssue.data.number}: ${issueTitle}`)
}
@@ -1,216 +0,0 @@
# Probe the highest allowed dependency versions, then open issues/PRs from the passing updates.
name: Python - Dependency Range Validation
on:
workflow_dispatch:
permissions:
contents: write
issues: write
pull-requests: write
env:
UV_CACHE_DIR: /tmp/.uv-cache
jobs:
dependency-range-validation:
name: Dependency Range Validation
runs-on: ubuntu-latest
env:
# For now only run 3.13, if we do encounter situations where there are mismatches between packages and python versions (other then 3.10 and 3.14 which are known to not be able to install everything)
# then we will have to reevaluate.
UV_PYTHON: "3.13"
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Set up python and install the project
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
env:
UV_CACHE_DIR: /tmp/.uv-cache
- name: Run dependency range validation
id: validate_ranges
# Keep workflow running so we can still publish diagnostics from this run.
continue-on-error: true
run: uv run poe validate-dependency-bounds-project --mode upper --package "*"
working-directory: ./python
- name: Upload dependency range report
# Always publish the report so failures are inspectable even when validation fails.
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: dependency-range-results
path: python/scripts/dependencies/dependency-range-results.json
if-no-files-found: warn
- name: Create issues for failed dependency candidates
# Always process the report so failed candidates create actionable tracking issues.
if: always()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const fs = require("fs")
const reportPath = "python/scripts/dependencies/dependency-range-results.json"
if (!fs.existsSync(reportPath)) {
core.warning(`No dependency range report found at ${reportPath}`)
return
}
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"))
const dependencyFailures = []
for (const packageResult of report.packages ?? []) {
for (const dependency of packageResult.dependencies ?? []) {
const candidateVersions = new Set(dependency.candidate_versions ?? [])
const failedAttempts = (dependency.attempts ?? []).filter(
(attempt) => attempt.status === "failed" && candidateVersions.has(attempt.trial_upper)
)
if (!failedAttempts.length) {
continue
}
const failuresByVersion = new Map()
for (const attempt of failedAttempts) {
const version = attempt.trial_upper || "unknown"
if (!failuresByVersion.has(version)) {
failuresByVersion.set(version, attempt.error || "No error output captured.")
}
}
dependencyFailures.push({
packageName: packageResult.package_name,
projectPath: packageResult.project_path,
dependencyName: dependency.name,
originalRequirements: dependency.original_requirements ?? [],
finalRequirements: dependency.final_requirements ?? [],
failedVersions: [...failuresByVersion.entries()].map(([version, error]) => ({ version, error })),
})
}
}
if (!dependencyFailures.length) {
core.info("No failing dependency candidates found.")
return
}
const owner = context.repo.owner
const repo = context.repo.repo
const openIssues = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: "open",
per_page: 100,
})
const openIssueTitles = new Set(
openIssues.filter((issue) => !issue.pull_request).map((issue) => issue.title)
)
const formatError = (message) => String(message || "No error output captured.").replace(/```/g, "'''")
for (const failure of dependencyFailures) {
const title = `Dependency validation failed: ${failure.dependencyName} (${failure.packageName})`
if (openIssueTitles.has(title)) {
core.info(`Issue already exists: ${title}`)
continue
}
const visibleFailures = failure.failedVersions.slice(0, 5)
const omittedCount = failure.failedVersions.length - visibleFailures.length
const failureDetails = visibleFailures
.map(
(entry) =>
`- \`${entry.version}\`\n\n\`\`\`\n${formatError(entry.error).slice(0, 3500)}\n\`\`\``
)
.join("\n\n")
const body = [
"Automated dependency range validation found candidate versions that failed checks.",
"",
`- Package: \`${failure.packageName}\``,
`- Project path: \`${failure.projectPath}\``,
`- Dependency: \`${failure.dependencyName}\``,
`- Original requirements: ${
failure.originalRequirements.length
? failure.originalRequirements.map((value) => `\`${value}\``).join(", ")
: "_none_"
}`,
`- Final requirements after run: ${
failure.finalRequirements.length
? failure.finalRequirements.map((value) => `\`${value}\``).join(", ")
: "_none_"
}`,
"",
"### Failed versions and errors",
failureDetails,
omittedCount > 0 ? `\n_Additional failed versions omitted: ${omittedCount}_` : "",
"",
`Workflow run: ${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`,
].join("\n")
await github.rest.issues.create({
owner,
repo,
title,
body,
})
openIssueTitles.add(title)
core.info(`Created issue: ${title}`)
}
- name: Refresh lockfile
# Only refresh lockfile after a clean validation to avoid committing known-bad ranges.
if: steps.validate_ranges.outcome == 'success'
run: uv lock --upgrade
working-directory: ./python
- name: Commit and push dependency updates
id: commit_updates
if: steps.validate_ranges.outcome == 'success'
run: |
BRANCH="automation/python-dependency-range-updates"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -B "${BRANCH}"
git add python/packages/*/pyproject.toml python/uv.lock
if git diff --cached --quiet; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No dependency updates to commit."
exit 0
fi
git commit -m "chore: update dependency ranges"
git push --force-with-lease --set-upstream origin "${BRANCH}"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
- name: Create or update pull request with GitHub CLI
# Only open/update PRs for validated updates to keep automation branches trustworthy.
if: steps.validate_ranges.outcome == 'success' && steps.commit_updates.outputs.has_changes == 'true'
run: |
BRANCH="automation/python-dependency-range-updates"
PR_TITLE="Python: chore: update dependency ranges"
PR_BODY_FILE="$(mktemp)"
cat > "${PR_BODY_FILE}" <<'EOF'
This PR was generated by the dependency range validation workflow.
- Ran `uv run poe validate-dependency-bounds-project --mode upper --package "*"`
- Updated package dependency bounds
- Refreshed `python/uv.lock` with `uv lock --upgrade`
EOF
PR_NUMBER="$(gh pr list --head "${BRANCH}" --base main --state open --json number --jq '.[0].number')"
if [ -n "${PR_NUMBER}" ]; then
gh pr edit "${PR_NUMBER}" --title "${PR_TITLE}" --body-file "${PR_BODY_FILE}"
else
gh pr create --base main --head "${BRANCH}" --title "${PR_TITLE}" --body-file "${PR_BODY_FILE}"
fi
@@ -1,91 +0,0 @@
name: Python - Dev Dependency Upgrade
on:
workflow_dispatch:
permissions:
contents: write
pull-requests: write
env:
UV_CACHE_DIR: /tmp/.uv-cache
jobs:
upgrade-dev-dependencies:
name: Upgrade Dev Dependencies
runs-on: ubuntu-latest
env:
UV_PYTHON: "3.13"
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Set up python and install the project
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
env:
UV_CACHE_DIR: /tmp/.uv-cache
- name: Upgrade dev dependencies and validate workspace
run: uv run poe upgrade-dev-dependencies
working-directory: ./python
- name: Commit and push dev dependency updates
id: commit_updates
run: |
BRANCH="automation/python-dev-dependency-updates"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -B "${BRANCH}"
git add python/pyproject.toml python/packages/*/pyproject.toml python/uv.lock
if git diff --cached --quiet; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No dev dependency updates to commit."
exit 0
fi
git commit -F- <<'EOF'
Python: chore: upgrade dev dependencies
EOF
git push --force-with-lease --set-upstream origin "${BRANCH}"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
- name: Create or update pull request with GitHub CLI
if: steps.commit_updates.outputs.has_changes == 'true'
run: |
BRANCH="automation/python-dev-dependency-updates"
PR_TITLE="Python: chore: upgrade dev dependencies"
PR_BODY_FILE="$(mktemp)"
cat > "${PR_BODY_FILE}" <<'EOF'
### Motivation and Context
This automated update refreshes Python dev dependency pins across the workspace and reruns the repo validation gates before opening a pull request.
### Description
- Ran `uv run poe upgrade-dev-dependencies`
- Refreshed dev dependency pins in workspace `pyproject.toml` files
- Refreshed `python/uv.lock` with `uv lock --upgrade`
- Reinstalled from the frozen lockfile and reran `check`, `typing`, and `test`
### Contribution Checklist
- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [Contribution Guidelines](https://github.com/microsoft/agent-framework/blob/main/CONTRIBUTING.md)
- [x] All unit tests pass, and I have added new tests where possible
- [ ] **Is this a breaking change?** If yes, add "[BREAKING]" prefix to the title of the PR.
EOF
PR_NUMBER="$(gh pr list --head "${BRANCH}" --base main --state open --json number --jq '.[0].number')"
if [ -n "${PR_NUMBER}" ]; then
gh pr edit "${PR_NUMBER}" --title "${PR_TITLE}" --body-file "${PR_BODY_FILE}"
else
gh pr create --base main --head "${BRANCH}" --title "${PR_TITLE}" --body-file "${PR_BODY_FILE}"
fi
-3
View File
@@ -92,9 +92,6 @@ jobs:
- name: Run lab type checking
run: cd packages/lab && uv run poe pyright
- name: Run lab mypy
run: cd packages/lab && uv run poe mypy
# Surface failing tests
- name: Surface failing tests
if: always()
@@ -30,21 +30,31 @@ jobs:
merge-multiple: true
- name: Display structure of downloaded files
run: ls
- name: Read and set PR number
# Need to read the PR number from the file saved in the previous workflow
# because the workflow_run event does not have access to the PR number
# The PR number is needed to post the comment on the PR
- name: Read and validate PR number
# Keep the artifact handoff aligned with the workflow run that produced it.
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
if [ ! -s pr_number ]; then
echo "PR number file 'pr_number' is missing or empty"
exit 1
fi
PR_NUMBER=$(cat pr_number)
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
ARTIFACT_PR_NUMBER=$(cat pr_number)
if ! [[ "$ARTIFACT_PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "::error::PR number file contains invalid content"
exit 1
fi
echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV"
PR_HEAD_SHA=$(gh pr view "$ARTIFACT_PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid')
if [ "$PR_HEAD_SHA" != "$RUN_HEAD_SHA" ]; then
echo "::error::PR head SHA does not match the triggering workflow run"
exit 1
fi
echo "PR_NUMBER=$ARTIFACT_PR_NUMBER" >> "$GITHUB_ENV"
- name: Pytest coverage comment
id: coverageComment
uses: MishaKav/pytest-coverage-comment@26f986d2599c288bb62f623d29c2da98609e9cd4 # v1.6.0
@@ -11,7 +11,7 @@ trigger:
kind: OnConversationStart
id: workflow_demo
actions:
- kind: InvokeAzureAgent
id: question_student
conversationId: =System.ConversationId
+144
View File
@@ -0,0 +1,144 @@
---
status: accepted
contact: eavanvalkenburg
date: 2026-06-11
deciders: eavanvalkenburg
---
# Python minimal hosting core and pluggable channels
## Context and Problem Statement
Agent Framework has several protocol-specific hosting surfaces. App authors who want one agent or workflow on multiple protocols must compose servers, routes, middleware, session handling, and lifecycle code by hand.
We will introduce a small Python hosting core that owns the common server shape and leaves protocol details inside channel packages. The first public contract must be intentionally narrow so Python can ship a base contract before adding identity linking, proactive delivery, or multicast behavior. Other language implementations may reuse the same conceptual boundary, but this ADR records the Python decision.
## Decision Drivers
- Keep the first host easy to explain: one app, one hostable target, one or more channels.
- Reuse Agent Framework's existing agent, workflow, session, history, and checkpoint primitives.
- Let channel packages own protocol parsing, protocol responses, authentication details, and native command surfaces.
- Make session continuity explicit through a channel-supplied `ChannelSession(isolation_key=...)`.
- Avoid approving cross-channel identity and delivery semantics before their safety model is reviewed.
## Considered Options
1. Keep only protocol-specific hosts.
2. Ship a large hosting core with identity linking, authorization, background delivery, active-channel routing, and multicast in v1.
3. Ship a minimal host/channel core now and track linking/multicast as follow-up work.
### Keep only protocol-specific hosts
- Good: no new abstraction or package surface.
- Neutral: each protocol can continue evolving independently.
- Bad: every multi-channel app still has to compose servers, lifecycle, and session handling by hand.
### Ship the large cross-channel host in v1
- Good: the richest cross-channel scenarios are available immediately.
- Neutral: the host becomes the natural place to demonstrate identity and delivery policy.
- Bad: v1 becomes a security-sensitive identity and delivery system before the safety model is reviewed.
### Ship the minimal core now
- Good: the host/channel boundary can be implemented, tested, and explained without solving linking and durable delivery at the same time.
- Neutral: apps that need richer behavior must build it locally or wait for ADR-0028 follow-up work.
- Bad: proactive delivery and multicast scenarios are deliberately absent from v1.
## Decision Outcome
Chosen option: **minimal host/channel core now, follow-up enhancements later**.
`AgentFrameworkHost` owns:
- one application object,
- one hostable target (`SupportsAgentRun` agent-compatible object or a `Workflow`), and
- one or more channels.
Channels own:
- contributed routes, middleware, commands, and lifecycle callbacks,
- protocol-native request parsing into `ChannelRequest`,
- protocol-native rendering of the originating response, and
- any channel-specific authentication or signature validation.
The host owns:
- route/lifecycle aggregation,
- invocation of the target,
- `ChannelSession(isolation_key=...)` to `AgentSession` resolution and caching,
- `reset_session(isolation_key=...)`,
- host-level middleware, including Foundry isolation middleware only when the Foundry hosting environment flag is present,
- invocation of per-channel hooks (`ChannelRunHook`, `ChannelResponseHook`, `ChannelStreamUpdateHook`), and
- workflow checkpoint wiring through an explicit `checkpoint_location`.
`ChannelIdentity`, when present, is request metadata only. In v1 it is not a linking, authorization, or delivery key.
### Trust boundary for `isolation_key`
The host treats `ChannelSession.isolation_key` as a session partition key, not as proof of identity. Channels or host middleware must authenticate and authorize any externally supplied value before passing it to the host. For example, a Responses caller must not be allowed to choose an arbitrary `previous_response_id` or header-derived key unless the platform or middleware has already established that the caller owns that conversation. The host deliberately does not infer that trust from the string itself.
### Hook ownership
Channels provide hook configuration and protocol-native context. The host invokes those hooks as part of the common invocation pipeline:
- `ChannelRunHook` runs after channel parsing and before target invocation.
- `ChannelResponseHook` runs after target invocation and before the originating channel serializes its response.
- `ChannelStreamUpdateHook` is applied by the host while the channel consumes streamed updates because streaming serialization is protocol-specific.
`ChannelStreamUpdateHook` is an update hook, not a final-response sanitizer. Channels that use it for redaction or filtering must also apply equivalent policy to any final response they render. Channels choose whether the response is streaming before run hooks execute.
This keeps hook call conventions centralized while leaving protocol payload parsing and response formatting in channel packages.
### State owned by v1
`state_dir` is limited to host-owned local files for reset-session aliases and workflow checkpoint path derivation. It does not store linked identities, active-channel state, response-routing state, continuation records, durable runner queues, or delivery attempts. Those storage concerns belong to ADR-0028.
## Non-goals for v1
The following are deliberately **not** part of the v1 contract:
- cross-channel identity linking (`IdentityLinker`, `local_identity_link`, or `agent-framework-hosting-entra`),
- identity allowlists or authorization policy (`IdentityAllowlist`, `AuthPolicy`),
- response routing beyond the originating channel (`ResponseTarget`, active channel, specific linked channel, `all_linked`),
- push or payload codecs (`ChannelPush`, `ChannelPushCodec`),
- background/continuation delivery,
- durable task runners (`DurableTaskRunner`, `InProcessTaskRunner`),
- retry/replay policy (`RetryPolicy`),
- fan-out, multicast, or all-linked delivery,
- confidentiality tiers and `LinkPolicy`, and
- a host-level multi-agent router.
These areas are follow-up enhancements covered by [ADR-0028](0028-hosting-linking-multicast-enhancements.md). They are not prerequisites for shipping or using the v1 host.
## Consequences
Positive:
- The host/channel model can be implemented and tested without designing a security-sensitive identity graph.
- Existing and new channel packages can share one Starlette app, middleware stack, lifecycle, and target invocation path.
- Session continuity is explicit and debuggable: two channels share history only when they produce the same `isolation_key`.
- Hook invocation is centralized in the host, so channels do not each invent the call convention.
Negative:
- Apps that need OAuth linking, allowlists, proactive messages, or multicast must continue to implement those behaviors outside the v1 host.
- Some richer cross-channel scenarios from the original design move to a separate decision and validation cycle.
- The host must document `isolation_key` trust clearly because it now provides the shared session boundary.
## Validation Gates
Before this ADR is accepted:
- A sample can expose one target on multiple channels with one `AgentFrameworkHost` and no handwritten Starlette route composition.
- Built-in channel tests prove that routes, commands, startup, and shutdown callbacks are contributed by channels and aggregated by the host.
- Session tests prove that identical `ChannelSession.isolation_key` values resolve to the same cached `AgentSession`, and `reset_session` rotates that mapping.
- Channel tests prove that each channel renders only its own originating response; there is no host-level push, multicast, or active-channel delivery path.
- Workflow tests or samples use an explicit `checkpoint_location`.
- Foundry isolation middleware is documented and covered by integration or contract tests, including the non-Foundry case where raw isolation headers are ignored.
- The v1 API and packages do not expose the removed symbols or packages listed in [Non-goals for v1](#non-goals-for-v1).
- The Python spec is updated to match this simplified contract and uses "public", "stable", or "released" terminology for Agent Framework APIs.
## More Information
- Follow-up linking and multicast ADR: [ADR-0028](0028-hosting-linking-multicast-enhancements.md)
@@ -0,0 +1,132 @@
---
status: proposed
contact: eavanvalkenburg
date: 2026-06-11
deciders: eavanvalkenburg
---
# Hosting linking and multicast enhancements
## Context and Problem Statement
[ADR-0027](0027-hosting-channels.md) defines the minimal v1 hosting core: originating-channel responses, explicit `ChannelSession.isolation_key`, and no host-level identity linking, push, multicast, background delivery, or durable runners.
This ADR tracks the richer cross-channel behaviors that were removed from v1. These enhancements are **follow-up work** and are **not prerequisites** for shipping, using, or stabilizing the v1 host/channel core.
## Decision Drivers
- Cross-channel continuity must not create accidental cross-user, cross-tenant, or cross-channel data leaks.
- Non-originating delivery must be observable, idempotent, retryable, and supportable.
- Protocol payloads must remain channel-native while still being safe to persist and replay.
- App authors need opt-in policy controls, not hidden defaults.
- The enhancement stack should layer on top of the v1 host without reshaping the minimal channel contract.
## Enhancement Areas
The follow-up design should cover these capabilities together because they share identity, storage, delivery, and replay concerns:
- **Cross-channel identity linking** — a user can connect multiple `ChannelIdentity` values to one channel-neutral `isolation_key`.
- **Authorization and allowlist policy** — channels or hosts can require verified identity, allow specific native identities or claims, and deny unknown callers.
- **Non-originating response delivery** — a run can respond somewhere other than the request's originating protocol when explicitly configured.
- **Active-channel routing** — delivery can target the most recently observed linked channel for an `isolation_key`.
- **Multicast / all-linked delivery** — delivery can fan out to every linked channel or a selected set.
- **Background runs and continuation tokens** — long-running requests can return immediately and complete later, with a polling/status fallback.
- **Durable delivery runners** — delivery work can survive process restarts and support dead-letter handling.
- **Retry and replay semantics** — delivery attempts are bounded, deduplicated, and safe to replay.
- **Payload serialization** — channel-specific payloads can be persisted, redacted, versioned, and reconstructed without losing protocol fidelity.
Candidate API names from the broader design (`IdentityLinker`, `IdentityAllowlist`, `AuthPolicy`, `ResponseTarget`, `ChannelPush`, `ChannelPushCodec`, `DurableTaskRunner`, `InProcessTaskRunner`, `RetryPolicy`, `LinkPolicy`) remain design vocabulary for this ADR. They are not approved v1 APIs.
## Considered Options
### Option A — Leave all behavior to applications
Applications implement linking, authorization, push, retry, and serialization independently.
- Good: the hosting core stays very small.
- Neutral: advanced apps can still build what they need.
- Bad: every app must solve the same security and delivery problems, likely inconsistently.
### Option B — Add the full enhancement stack to v1
The first host release includes linking, authorization, active channel, multicast, background runs, durable runners, and codecs.
- Good: the original cross-channel experience is available immediately.
- Neutral: samples can demonstrate rich end-to-end flows.
- Bad: v1 becomes security-sensitive, storage-heavy, and harder to stabilize.
### Option C — Layer opt-in enhancement packages after v1
Ship the minimal host first, then add linking, authorization, and delivery packages behind explicit configuration.
- Good: v1 remains simple while leaving room for a reviewed, supportable enhancement stack.
- Neutral: apps that need advanced delivery wait for follow-up packages.
- Bad: the first release does not satisfy proactive or all-linked scenarios.
### Option D — Build only platform-specific integrations
Implement linking and proactive delivery separately in Telegram, Activity Protocol, Discord, and future channels.
- Good: each package can match its protocol exactly.
- Neutral: some shared abstractions may emerge later.
- Bad: cross-channel behavior becomes fragmented and hard to reason about.
## Decision Outcome
Proposed direction: **Option C — layered opt-in enhancement packages after v1**.
The minimal host remains the foundation. Follow-up packages may add linking, authorization, delivery, and durable execution, but must be explicitly enabled and must pass the validation gates below before becoming part of the public contract.
## Safety Requirements
### Threat model
The design must account for:
- spoofed channel-native identities,
- stolen or replayed link challenges,
- cross-tenant or cross-confidentiality data leakage,
- unsolicited proactive messages,
- malicious payloads persisted for replay,
- denial-of-service through fan-out or retry storms, and
- privacy leakage through logs, metrics, or support tooling.
Required mitigations include verified identity claims where available, signed and expiring link challenges, explicit user consent, per-channel capability checks, default-deny policy options, tenant partitioning, and uninformative denial messages on shared channels.
### Idempotency and replay
Exactly-once delivery is not a realistic guarantee. The design must provide:
- stable run, continuation, and delivery-attempt identifiers,
- channel-level idempotency keys where protocols support them,
- bounded retry with jitter and explicit terminal states,
- replay windows and expiration,
- duplicate suppression for persisted attempts, and
- clear semantics for "delivered", "accepted by platform", and "observed by user".
### Storage
Enhancement storage must stay distinct from v1 `AgentSession` history and workflow checkpoints unless an implementation deliberately backs them with the same physical store.
Stored data should be schema-versioned, minimized, encrypted or otherwise protected as appropriate, and partitioned by tenant/project. Link records, continuation records, active-channel state, delivery attempts, dead letters, and serialized payloads need independent TTL and deletion policies.
### Observability and support
The design must include structured logs, traces, and metrics for link attempts, authorization decisions, delivery scheduling, retries, replay, and dead-letter outcomes. Logs must avoid message content and sensitive identity claims by default. Operators need a way to inspect, revoke, replay, or purge stuck records safely.
## Validation Gates
Before these enhancements are accepted:
- A reviewed threat model covers identity linking, authorization, non-originating delivery, multicast, and replay.
- Cross-channel linking tests prove a verified identity can link two channels and that unlink/deny paths do not leak information.
- Authorization tests cover native-id allowlists, verified-claim allowlists, default-deny behavior, and misconfiguration failures.
- Delivery tests cover originating-only, specific-channel, active-channel, selected-channel, and all-linked routing.
- Background/continuation tests cover polling fallback, cancellation or expiration, process restart, retry, and dead-letter behavior.
- Codec tests prove payloads are versioned, redacted where needed, backward compatible, and rejected safely when unknown.
- Multicast tests prove fan-out is bounded, independently retried, and idempotent per destination.
- Observability tests or manual validation prove support operators can correlate a request to delivery attempts without exposing sensitive content.
## Relationship to ADR-0027
ADR-0027 remains valid without any of these enhancements. This ADR extends the hosting model only after the safety, storage, and support requirements above are satisfied.
@@ -0,0 +1,641 @@
---
status: proposed
contact: sergeymenshykh
date: 2026-06-23
deciders: sergeymenshykh
---
# Skills Over MCP: Implementation Design Options
This document explores design options for two SEP-2640 features. The decisions are not yet finalized.
- **Part 1: MCP Resource Template Skills** - skills described by a URI template with variables that must be resolved before loading.
- **Part 2: Direct Skill References** - reading `skill://` URIs referenced directly (e.g., in server instructions) without being listed in the index.
## Part 1: MCP Resource Template Skills
### Context and Problem Statement
The `AgentMcpSkillsSource` currently only supports `skill-md` type entries from `skill://index.json` (support for `archive` type is planned). The SEP-2640 specification also defines `mcp-resource-template` entries: **parameterized skill namespaces** described by a URI template with variables (e.g., `{product}`) that resolve to concrete `SKILL.md` URIs. Rather than materializing every skill in the index, the template's variables must be resolved before a skill can be loaded.
### Index Entry Format
```json
{
"$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
"skills": [
{
"name": "git-workflow",
"type": "skill-md",
"description": "Follow this team's Git conventions for branching and commits",
"url": "skill://git-workflow/SKILL.md"
},
{
"type": "mcp-resource-template",
"description": "Per-product documentation skill",
"url": "skill://docs/{product}/SKILL.md"
}
]
}
```
Key differences from `skill-md`:
| Field | `skill-md` | `mcp-resource-template` |
|-------|------------|-------------------------|
| `name` | Required (the skill name) | **Omitted** (represents many skills) |
| `type` | `"skill-md"` | `"mcp-resource-template"` |
| `url` | Concrete URI to `SKILL.md` | URI template with variables |
| `description` | Describes the skill | Describes the addressable skill space |
### Use Cases
Template skills address two scenarios where listing concrete skills is impractical:
- **Large skill catalogs** - too many skills to enumerate every entry in the index.
- **Dynamically generated skills** - skill content generated on the fly from parameters, so the set of valid skills is not known at index-creation time.
### How Template Skills Are Consumed
Per SEP-2640, the consumption flow relies on the MCP `completion/complete` method:
1. **Server registers a resource template** - The MCP server registers the same `url` value (e.g., `skill://docs/{product}/SKILL.md`) as an MCP [resource template](https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resource-templates), wiring template variables to the [completion API](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion).
2. **Host reads `skill://index.json`** - Discovers the template entry with `type: "mcp-resource-template"`.
3. **Host surfaces template in UI** - Presents the template as an interactive discovery point where the user fills in variables.
4. **Host calls `completion/complete`** - For each template variable (e.g., `{product}`), the host calls the MCP completion API to get possible values from the server:
```json
{
"method": "completion/complete",
"params": {
"ref": {
"type": "ref/resource",
"uri": "skill://docs/{product}/SKILL.md"
},
"argument": {
"name": "product",
"value": ""
}
}
}
```
The server responds with possible completions:
```json
{
"completion": {
"values": ["widgets", "billing", "auth", "payments"],
"hasMore": false,
"total": 4
}
}
```
5. **User selects a value** - The user picks a value (e.g., `"billing"`) from the list.
6. **Host resolves the URI** - The template `skill://docs/{product}/SKILL.md` becomes the concrete URI `skill://docs/billing/SKILL.md`.
7. **Host reads the resolved skill** - Calls `resources/read` with the concrete URI and proceeds as with any `skill-md` skill.
### Potential Implementation Options
### Option 1: Callback on `AgentMcpSkillsSource` for Variable Value Selection
Add a callback to `AgentMcpSkillsSource` (or its options) that is invoked for each `mcp-resource-template` entry to let the caller select variable values.
**Flow:**
1. `AgentMcpSkillsSource.GetSkillsAsync()` reads `skill://index.json`
2. For each entry with `type: "mcp-resource-template"`:
- Parse the URI template to extract variable names (e.g., `{product}`)
- Call the MCP `completion/complete` API to get possible values for each variable
- Invoke the caller-provided callback with the variable name, description, and possible values
- The callback returns a selected value and a `bool` indicating whether to include the skill
3. Resolve the URI template with the selected values
4. Create an `AgentMcpSkill` from the resolved URI and add it to the skills list
**API sketch:**
```csharp
public delegate Task<(string? SelectedValue, bool IncludeSkill)> McpTemplateVariableSelector(
string templateDescription,
string variableName,
IReadOnlyList<string> possibleValues,
CancellationToken cancellationToken);
// Usage via builder:
var provider = new AgentSkillsProviderBuilder()
.UseMcpSkills(mcpClient, options => {
options.TemplateVariableSelector = async (description, variable, values, ct) =>
{
// Present to user, return selection
var selected = PromptUser(variable, values);
return (selected, IncludeSkill: selected is not null);
};
})
.Build();
```
**Pros:**
- Simple implementation
- Easy to understand and use
**Cons:**
- Cannot be used in server-side scenarios where there is no interactive user at skill-discovery time
- Does not integrate with the agent's conversational flow
---
### Option 2: Integrate into Agent Conversation via `ChatClientAgent` Decorator
Model the template variable resolution as a request/response interaction within the agent's conversational loop.
**Flow:**
1. A `DelegatingAIAgent` decorator (e.g., `McpTemplateSkillResolutionAgent`) intercepts `RunAsync`/`RunStreamingAsync` calls and checks whether the inner agent has an `AgentSkillsProvider` with an `AgentMcpSkillsSource` containing unresolved template entries. The check is performed via `GetService<AgentMcpSkillsSource>()` on the `AgentSkillsProvider`, which delegates to a `GetService` method on the `AgentSkillsSource` base class.
2. The decorator calls an internal member on `AgentMcpSkillsSource` to get the list of `mcp-resource-template` entries from the index. The `AgentMcpSkillsSource` needs to be extended with an internal member that exposes unresolved template entries separately from concrete skills.
3. For each template entry, the decorator calls an internal member on `AgentMcpSkillsSource` to retrieve possible values for the template's variables via the MCP `completion/complete` API.
4. For each variable needing resolution, the decorator returns an `McpResourceTemplateValueRequestContent` (inherits from MEAI's `InputRequestContent`) in the agent response - bypassing the call to the inner agent. The content carries the template description, variable name, and possible values.
5. The user app receives the response, identifies the `McpResourceTemplateValueRequestContent` content type, and displays UI to the user showing the variable name and possible values, or forwards it further downstream if the user app is a service.
6. The user selects a value, and the user app calls the agent again with a corresponding `McpResourceTemplateValueResponseContent` (inherits from MEAI's `InputResponseContent`) containing the selected value. The `RequestId` property (inherited from the base classes) correlates the response with the original request.
7. The decorator identifies the response content and provides the resolved values to `AgentMcpSkillsSource` so it can use them when constructing concrete skills.
8. Having resolved all template variables, the decorator calls `RunAsync`/`RunStreamingAsync` on the inner agent.
9. The inner agent invokes the `AgentSkillsProvider`, which calls `AgentMcpSkillsSource.GetSkillsAsync()`. The source now has all resolved variable values and constructs concrete `AgentMcpSkill` instances from the resolved URIs, so it can provide the skill content if requested by the model.
**API sketch:**
```csharp
// New content types inheriting from MEAI's InputRequestContent/InputResponseContent:
public sealed class McpResourceTemplateValueRequestContent : InputRequestContent
{
public string TemplateDescription { get; }
public string VariableName { get; }
public IReadOnlyList<string> PossibleValues { get; }
public string TemplateUrl { get; }
}
public sealed class McpResourceTemplateValueResponseContent : InputResponseContent
{
public string SelectedValue { get; }
public string TemplateUrl { get; }
}
// Decorator usage:
var provider = new AgentSkillsProviderBuilder()
.UseMcpSkills(mcpClient)
.Build();
AIAgent agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions
{
AIContextProviders = [provider],
});
agent = new McpTemplateSkillResolutionAgent(agent);
```
**Pros:**
- Works in server-side scenarios
- Fits the existing `DelegatingAIAgent` decorator pattern
- Can be composed with other decorators (tool approval, etc.)
**Cons:**
- Complex implementation
- Requires user app awareness of the new content types
- Users need to know that an additional decorator is required for handling MCP template skills, in addition to registering the MCP skills source
- Resolved template variable values must be persisted across conversation turns so the decorator does not re-prompt on subsequent agent runs within the same session
**Note:** This writeup is high-level and may miss details that could change the design. A POC would be needed to validate the approach.
### Open Questions
1. **Completion API limit** - The MCP completion API returns at most 100 values per request and provides no offset/cursor mechanism for enumeration. If a variable has more than 100 possible values, it's unclear how to retrieve the rest - the API only supports prefix-based filtering (typeahead), not bulk pagination.
2. **Multi-variable templates** - A template like `skill://{org}/{product}/SKILL.md` has multiple variables. Should they be resolved sequentially (org first, then product - since product values may depend on org) or presented together?
3. **Caching** - Should resolved template values be saved in the `AgentSession` so the user isn't re-prompted on every agent run? How should they be persisted between sessions?
---
## Part 2: Direct Skill References
This part covers how to let the model read `skill://` URIs referenced directly (e.g., in an MCP server's `instructions`, in a resource, or in another skill's content) without being listed in `skill://index.json`.
### How MCP Skills and Relative Links Work Today
The `AgentMcpSkillsSource` discovers skills by reading the well-known `skill://index.json` resource from the MCP server:
```json
{
"$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
"skills": [
{
"name": "unit-converter",
"type": "skill-md",
"description": "Convert between common units.",
"url": "skill://unit-converter/SKILL.md"
},
{
"name": "currency-converter",
"type": "skill-md",
"description": "Convert between world currencies using live rates.",
"url": "skill://currency-converter/SKILL.md"
}
]
}
```
For each `skill-md` entry it creates an `AgentMcpSkill` instance - frontmatter (name/description) comes straight from the entry. The `AgentSkillsProvider` lists the discovered skills in the model's context (name + description):
```xml
<available_skills>
<skill>
<name>unit-converter</name>
<description>Convert between common units.</description>
</skill>
<skill>
<name>currency-converter</name>
<description>Convert between world currencies using live rates.</description>
</skill>
</available_skills>
```
It also provides functions to the model so it can load a skill and access its resources:
```csharp
// Loads the full content of a specific skill.
load_skill(string skillName)
// Reads a resource associated with a skill (references, assets, dynamic data).
read_skill_resource(string skillName, string resourceName)
```
The model calls `load_skill("unit-converter")` and receives the skill content:
```markdown
---
name: unit-converter
description: Convert between common units.
---
## Usage
For the full conversion table, see references/units-table.md.
```
The skill body references `references/units-table.md` by relative path. The model calls `read_skill_resource("unit-converter", "references/units-table.md")` and receives the resource content:
```markdown
# Unit Conversion Table
| From | To | Factor |
| miles | km | 1.60934 |
| kg | lbs | 2.20462 |
```
### Direct Reference Examples
A `skill://` URI can appear in any of these locations:
**Server instructions** - the MCP server advertises a skill the model should load:
```text
Follow our coding standards. Load skill://code-standards/SKILL.md for details.
```
**A skill body** - a skill's `SKILL.md` links to a sibling resource:
```markdown
---
name: code-standards
description: Coding standards and conventions.
---
## Naming
Follow the naming rules in skill://code-standards/references/naming.md.
```
**A resource** - the linked resource holds the actual content:
```markdown
# Naming Rules
- Use PascalCase for public members and type names.
- Use camelCase for locals and parameters.
- Prefix interfaces with `I` (e.g. `ISkillReader`).
- Suffix async methods with `Async`.
For examples, see skill://code-standards/references/naming-examples.md.
```
How can the model access content by direct reference?
### Function for Reading Direct Skill References
### Option 1: Extend existing `load_skill` and `read_skill_resource` functions
```csharp
// Added optional 'origin' and a direct skill:// URI is passed in 'skillName'.
load_skill(string skillName, string? origin = null)
// Added optional 'origin', made 'skillName' optional, and a direct skill:// URI is passed in 'resourceName'.
read_skill_resource(string resourceName, string? skillName = null, string? origin = null)
```
The optional `origin` identifies the source/MCP server that should handle the direct URI.
| Case | Call |
|------|------|
| Load skill | `load_skill("commit-guidelines")` |
| Relative resource | `read_skill_resource("commit-guidelines", "examples/COMMIT_EXAMPLES.md")` |
| `skill://` link (skill) | `load_skill(skillName: "skill://commit-guidelines/SKILL.md", origin: "DirectRefServer")` |
| `skill://` link (resource) | `read_skill_resource(resourceName: "skill://commit-guidelines/examples/COMMIT_EXAMPLES.md", origin: "DirectRefServer")` |
**Pros:**
- No new functions added: existing tool surface stays at two functions.
**Cons:**
- Unreliable on some models (gpt-4o, gpt-4.1-mini): it often omits `origin` when it should not or calls the wrong function.
- Optional parameters create silent ambiguity - the model can pass `origin` for non-MCP skills or omit it for `skill://` URIs.
### Option 2 (Proposed): Add a dedicated `read_skill_uri` function alongside existing ones
```csharp
// Existing functions stay unchanged.
load_skill(string skillName)
read_skill_resource(string skillName, string resourceName)
// New function added alongside: reads content by direct skill:// URI.
read_skill_uri(string uri, string origin)
```
| Case | Call |
|------|------|
| Load skill | `load_skill("commit-guidelines")` |
| Relative resource | `read_skill_resource("commit-guidelines", "examples/COMMIT_EXAMPLES.md")` |
| `skill://` link (skill) | `read_skill_uri(uri: "skill://commit-guidelines/SKILL.md", origin:"DirectRefServer")` |
| `skill://` link (resource) | `read_skill_uri(uri: "skill://commit-guidelines/examples/COMMIT_EXAMPLES.md", origin: "DirectRefServer")` |
**Pros:**
- Purely additive - no changes to existing functions needed; `read_skill_uri` can be deferred and added later when direct `skill://` reference support is needed.
- Granular approval: each function can have its own approval gate (like the existing `ScriptApproval` for `run_skill_script`), making per-operation approval for skill loading, resource reading, and direct URI access straightforward to add.
- Both `uri` and `origin` are required - no silent misuse through optional parameters.
- Clean split: `load_skill`/`read_skill_resource` for named skills, `read_skill_uri` for `skill://` links - no parameter ambiguity.
**Cons:**
- Three read functions (`load_skill`, `read_skill_resource`, `read_skill_uri`), not counting `run_skill_script`: larger tool surface than a single-function design.
### Option 3: Collapse `load_skill` and `read_skill_resource` into a single `read_resource` function
```csharp
// Single entrypoint for all skill content. 'uri' is required; 'origin' is optional.
read_resource(string uri, string? origin = null)
```
- `uri` - what to read: a skill name, a relative resource path, or a `skill://` link.
- `origin` - determines how `uri` is interpreted:
- **omitted** → load skill by name (`uri` is the skill name).
- **skill name** → read a relative resource (`uri` is the path within that skill).
- **server name** → read content by the `skill://` link (`uri` is handled by the source identified by the `[Origin: X]` marker).
Dispatch is ordered: null `origin` routes to Case 1; if `origin` names a known skill, routes to Case 2; otherwise tries to find an `ISkillUriReader` whose `CanRead` returns true for `origin` (Case 3).
| Case | Call |
|------|------|
| Load skill | `read_resource(uri: "commit-guidelines")` |
| Relative resource | `read_resource(uri: "examples/COMMIT_EXAMPLES.md", origin: "commit-guidelines")` |
| `skill://` link (skill) | `read_resource(uri: "skill://commit-guidelines/SKILL.md", origin: "DirectRefServer")` |
| `skill://` link (resource) | `read_resource(uri: "skill://commit-guidelines/examples/COMMIT_EXAMPLES.md", origin: "DirectRefServer")` |
**Pros:**
- Minimal tool surface: one read function instead of two or three (not counting `run_skill_script`) reduces token usage and gives the model fewer choices.
**Cons:**
- No per-operation approval: all cases (skill loading, resource reading, direct URI access) share one function, so approval cannot be scoped to individual operations.
- Unreliable on gpt-4.1-mini: omits `origin` when reading `skill://` links, passes skill name as `origin` when loading a plain skill (should be omitted), and hallucinates resource names (e.g. `API_SPECIFICATION.md`) that do not exist.
---
### Origin Marker
A `skill://` URI does not carry an origin, but the model needs to provide one when reading it. The `origin` is what routes the read call to the source that can handle the URI - the provider uses it to pick the matching source. Since the URI itself carries no such hint, the MCP source injects an `[Origin: ...]` marker wherever a `skill://` URI appears, so the model can read it back and pass it as the `origin` argument.
The marker is only added when the content actually contains `skill://` references. If a piece of content (server instructions, a skill body, or a resource) has no `skill://` URIs, there is nothing for the model to read back, so no marker is injected.
Into **server instructions**, which may mention `skill://` URIs directly:
```
[Origin: code-standards-server]
Follow our coding standards. Load skill://code-standards/SKILL.md for details.
```
Into **skill bodies**, since a `SKILL.md` may reference other `skill://` URIs (a resource file or a related skill):
```
[Origin: code-standards-server]
# Code Standards
For naming conventions, load skill://code-standards/references/naming.md.
```
Into **skill resources**, since a resource may itself reference further `skill://` URIs:
```
[Origin: code-standards-server]
# Naming Rules
- Use PascalCase for public members and type names.
- Use camelCase for locals and parameters.
For examples, see skill://code-standards/references/naming-examples.md.
```
---
### Read-by-URI Capability: Interface vs Base Class Virtual Methods
Now let's look at how an `AgentSkillsSource` can opt in to reading `skill://` URIs and signal that capability to the provider.
### Option 1: New `ISkillUriReader` interface
```csharp
public interface ISkillUriReader
{
// Returns true if this reader can handle the given skill:// URI from the given origin.
bool CanRead(string uri, string origin);
// Reads and returns the content for the given skill:// URI.
Task<object?> ReadByUriAsync(string uri, string origin, CancellationToken cancellationToken = default);
}
```
Sources that support direct `skill://` URI reads - such as `AgentMcpSkillsSource` - implement this interface to opt in.
The provider discovers readers via a service locator and dispatches to the first that can handle the URI:
```csharp
// Discover all registered readers.
var readers = source.GetService<IEnumerable<ISkillUriReader>>();
// Pick the first reader that can handle the URI.
var reader = readers.FirstOrDefault(r => r.CanRead(uri, origin))
?? throw new InvalidOperationException($"No reader can handle URI '{uri}' from origin '{origin}'.");
// Delegate the read to it.
return await reader.ReadByUriAsync(uri, origin, cancellationToken);
```
The provider may treat a source implementing `ISkillUriReader` as the signal to advertise `read_skill_uri`: if at least one registered source implements the interface, the function is exposed to the model; otherwise it is not.
### Option 2 (Proposed): Virtual methods on `AgentSkillsSource` base class
```csharp
public abstract class AgentSkillsSource
{
// New members for reading by URI.
// Whether this source can read by URI; drives whether read_skill_uri is advertised. Off by default.
public virtual bool SupportsReadByUri => false;
// Returns true if this source can handle the given skill:// URI from the given origin.
public virtual bool CanReadByUri(string uri, string origin) => false;
// Reads and returns the content for the given skill:// URI.
public virtual Task<object?> ReadByUriAsync(string uri, string origin, CancellationToken cancellationToken = default)
=> Task.FromResult<object?>(null);
// Existing member.
public abstract Task<IList<AgentSkills>> GetSkillsAsync(CancellationToken cancellationToken = default);
}
```
Sources opt in by overriding, and the provider calls them directly:
```csharp
// AgentMcpSkillsSource opts in by overriding the virtuals.
public override bool SupportsReadByUri => true;
// Handles the URI when its origin matches this source's MCP server.
public override bool CanReadByUri(string uri, string origin)
=> string.Equals(origin, this.Origin, StringComparison.OrdinalIgnoreCase);
// Reads content by skill:// URI from the MCP server.
public override Task<string?> ReadByUriAsync(string uri, string origin, CancellationToken cancellationToken)
=> /* resolve uri via the MCP server identified by origin */;
```
All sources inherit the methods, so there is no type signal - `SupportsReadByUri` fills that role. The function is advertised when any registered source returns `true`.
### Comparison
| Aspect | Option 1: Interface | Option 2: Base class virtual methods |
|--------|---------------------|--------------------------------------|
| Discovery | Service locator | Direct call on source |
| Advertising signal | Interface implementation | `SupportsReadByUri` flag |
| Adding new members | Breaking change | Non-breaking |
| Complexity | Higher | Lower |
---
### Include MCP Server Instructions Into Agent Instructions
MCP server instructions may contain the `skill://` references the model needs, so we want to surface them in the agent's instructions. But they can also carry system prompts or behavioral directives irrelevant to the agent, polluting context - so inclusion is **opt-in** via the `IncludeServerInstructions` option:
```csharp
public sealed class AgentMcpSkillsSourceOptions
{
// When true, the MCP server's instructions are injected into the agent instructions. Off by default.
public bool IncludeServerInstructions { get; set; }
}
builder.UseMcpSkills(mcpClient, options => options.IncludeServerInstructions = true);
```
When enabled, the instructions travel alongside the discovered skills on `AgentSkillsResult`:
```csharp
public class AgentSkillsResult
{
// The skills discovered from the source.
public IList<AgentSkill> Skills { get; }
// The MCP server instructions, when IncludeServerInstructions is enabled; otherwise null.
public string? Instructions { get; }
}
```
The `AgentSkillsProvider` then appends them to its own skill-usage guidance when building the agent's instructions:
```csharp
var result = await source.GetSkillsAsync(cancellationToken);
var instructions = DefaultSkillsInstructionPrompt;
if (!string.IsNullOrWhiteSpace(result.Instructions))
{
// Combine the provider's skill-usage guidance with the server instructions.
instructions += Environment.NewLine + result.Instructions;
}
```
### Enabling Direct Skill References
Following direct `skill://` references is **disabled by default** and activated via an option. When enabled, the provider advertises the read function to the model, and the source injects the `[Origin: ...]` marker into all content provided by the MCP server that contains `skill://` references. When disabled, no function is advertised and no marker is injected.
```csharp
public sealed class AgentMcpSkillsSourceOptions
{
public bool EnableDirectReferences { get; set; }
}
builder.UseMcpSkills(mcpClient, options => options.EnableDirectReferences = true);
```
## Decision Outcome
### Template Variable Resolution: Callback vs Decorator (Part 1)
**Postponed.** Deferring this decision until:
- We have a concrete list of scenarios that require template variable resolution.
- The skills-over-MCP spec is released (it is still a draft, so the design may change).
- There is a strong signal of demand from users or the ecosystem.
### Function for Reading Direct Skill References (Part 2)
**Postponed.** Leaning toward **Option 2 - dedicated `read_skill_uri` function alongside existing ones** (purely additive, and each function can have its own approval gate for granular per-operation approval), but deferring the decision until:
- The skills-over-MCP spec is released (it is still a draft, so the design may change).
- There is a strong signal of demand from users or the ecosystem.
### Read-by-URI Capability: Interface vs Base Class (Part 2)
**Postponed.** Leaning toward **Option 2 - virtual methods on `AgentSkillsSource`** (non-breaking, lower complexity, and a natural fit with the existing base class hierarchy), but deferring the decision until:
- The skills-over-MCP spec is released (it is still a draft, so the design may change).
- There is a strong signal of demand from users or the ecosystem.
The method naming (`SupportsReadByUri`, `CanReadByUri`, `ReadByUriAsync`) should also be abstracted a little more before adoption, so the same members can be reused when a similar direct-reference concept is needed for other skill types (e.g. file skills).
## References
- [SEP-2640: Skills Extension](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640) - Draft proposal
- [SEP-2640 Implementation Guidelines: Model-Driven Resource Loading](https://github.com/modelcontextprotocol/experimental-ext-skills/blob/main/docs/sep-draft-skills-extension.md#hosts-model-driven-resource-loading)
- [MCP Completion API](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion) - Used for template variable resolution
- [MCP Resource Templates](https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resource-templates)
- [Skills Over MCP Working Group](https://github.com/modelcontextprotocol/experimental-ext-skills)
- [Open Question #4: Multi-server skill dependencies](https://github.com/modelcontextprotocol/experimental-ext-skills/issues/39)
- [Anthropic Agent Skills - Overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) - Prior art: single skill entrypoint + generic file reads
- [Anthropic Agent Skills in the SDK](https://code.claude.com/docs/en/agent-sdk/skills) - The `Skill` tool exposed to the model
@@ -0,0 +1,356 @@
---
status: accepted
contact: eavanvalkenburg
date: 2026-06-19
deciders: eavanvalkenburg, moonbox3, TaoChenOSU, chetantoshnival
consulted: westey-m
informed:
---
# Python identity lifetimes for sessions, tasks, and continuation
## Context and Problem Statement
Python `AgentSession` currently carries a local `session_id`, an optional opaque service continuation
`service_session_id`, and provider state. `service_session_id` is any service-owned value that lets that service continue
a conversation, session, or thread; chat clients happen to map it through the abstract `conversation_id` ChatOption, but
other agent types can use it differently. It is not a generic correlation field, and generic correlation should not
require parsing or understanding that opaque service-owned value.
The related issues mix values with different lifetimes:
- **Session / conversation identity**: values that group a multi-turn interaction. Examples: A2A `context_id`, OpenAI
Responses `conversation` (`conv_*`) or response-chain continuation (`previous_response_id`).
- **Task identity**: values that identify a protocol task and may affect future protocol calls. Example: A2A `task_id`.
- **Message / response identity**: values that identify an output message or response. Examples: A2A `message_id` /
`artifact_id`, OpenAI Responses response id (`resp_*`).
- **Continuation token**: a framework resume payload for in-progress work. It may contain the same underlying value as a
protocol id, such as A2A `task_id`, but it only exists when there is an unfinished operation to resume.
These values should not automatically live in the same object just because they all help "continue" something. A value
belongs in `AgentSession` only when it is needed to continue future calls across turns. A value that identifies one
result belongs on the response or message. A value that resumes in-progress work belongs in a `ContinuationToken`.
An `AgentSession` created for one agent is not expected to be guaranteed to work against another agent. When a session is
used with an incompatible agent, protocol, or service, the framework should still help users understand what is wrong as
early as possible, preferably before calling out to the remote service.
For #4673, native conversation identity propagation should be based on `AgentSession` where the value is durable session
state. For #4893, A2A `context_id` and `task_id` need a coherent Agent Framework mapping.
AG-UI is out of scope for the decision. Its `thread_id` already maps to `AgentSession.session_id` in the normal wrapper
path, and `run_id` is wrapper-owned event correlation. If AG-UI run correlation needs framework telemetry integration
later, that should be handled as a run-context/telemetry design, not as session identity.
### Concrete gap example
At the protocol level, the durable continuation payload shapes are different:
```json
// A2A: future calls may need multiple durable protocol fields
{
"context_id": "ctx_123",
"task_id": "task_789",
"task_state": "input_required"
}
```
```json
// OpenAI Responses: future calls usually need one continuation value
{
"previous_response_id": "resp_abc123"
}
```
The gap is that A2A continuation state is multi-field while OpenAI continuation is
typically single-field.
## Current implementation notes
- A2A currently has `A2AAgentSession`, but `A2AAgent.create_session(...)` does not automatically return it.
- A2A currently mirrors `context_id` into `service_session_id`; that is current behavior, not necessarily the target
abstraction.
- A2A `task_id` is not just cosmetic correlation. It is used for `task_id` when a task is `INPUT_REQUIRED`, for
`reference_task_ids` when refining a previous task, and inside `A2AContinuationToken` for in-progress tasks.
- `RawAgent._prepare_run_context(...)` currently forwards `active_session.service_session_id` as chat `conversation_id`,
so any non-string or formatted value affects existing chat-client paths.
- `OpenAIChatClient` maps chat options `conversation_id` to the Responses API as `previous_response_id` for `resp_*`,
`conversation` for `conv_*`, and defaults unrecognized strings to `previous_response_id`. When `store` is not `False`,
it returns `response.conversation.id` when available, otherwise `response.id`, as the next service continuation value.
- For Responses API, the response id (`resp_*`) is also the response/message identity surfaced as
`ChatResponse.response_id`; when used for continuation on the next request, it becomes the `previous_response_id`
value.
- Python A2A has not been released as stable yet, so its session factory or session shape can still be adjusted before
release.
## Decision Drivers
- Preserve `AgentSession.session_id` as the local/client conversation identity.
- Preserve `AgentSession.service_session_id` as an opaque service-owned continuation handle.
- Keep `AgentSession` for durable state needed across turns, not per-run bookkeeping.
- Store values needed by future calls in durable session state; keep values that only resume in-progress work in
`ContinuationToken`.
- Fix the current confusion where session, task, response, and continuation values can be treated as interchangeable
because they all participate in "continuing" something.
- Make the implementation following this ADR preserve the lifetime split clearly: future-call state, in-progress resume
tokens, response/message ids, and protocol event correlation must not be silently mixed.
- Expose durable continuation state in a typed way when future calls depend on it.
- Let telemetry correlate runs without parsing opaque service continuation handles.
- Reuse existing run/context surfaces before introducing a new identity abstraction.
- Keep MCP and other remote tool boundaries safe: framework identity must not be forwarded to remote tools unless an
existing explicit opt-in mechanism says so.
- Keep existing `AgentSession.to_dict()` / `from_dict()` migration and compatibility straightforward.
- Stay close to .NET where there is already behavior to match, especially A2A's `ContextId`, `TaskId`, and `TaskState`.
- Detect incompatible session identity shapes as early as practical, preferably before a remote service call.
## Non-goals
- Do not design a provider-agnostic conversation creation API here. That is tracked separately in #6622.
- Do not make `service_session_id` a generic telemetry or run-correlation field.
- Do not introduce a new identity object if existing run/context objects can carry the selected per-run correlation value.
- Do not make a session from one agent guaranteed to work against another agent.
- Do not optimize the public `agent.run(...)` API for protocol-wrapper internals.
## Remaining question: durable shape for additional continuation state
- Option A: Use protocol-specific `AgentSession` subclasses.
- Option B: Extend `service_session_id` with richer service-owned values.
- Option C: Add a dedicated dict for additional session details.
- Option D: Store additional durable state inside `AgentSession.state`.
### Option A: Use protocol-specific `AgentSession` subclasses
Each protocol or agent type that needs additional durable state keeps a specialized `AgentSession` subclass. For A2A,
that means keeping `A2AAgentSession` for A2A-specific durable state and changing `A2AAgent.create_session(...)` to return
that type.
Example:
```python
# First call returns a task that future A2A messages may need to reference.
session = await a2a_agent.create_session()
response = await a2a_agent.run(
message,
session=session,
)
# A2AAgent updates durable A2A protocol state from the returned task/status payload.
# The user does not set these manually.
assert isinstance(session, A2AAgentSession)
assert session.task_id is not None
assert session.task_state is not None
# Later call reuses the durable A2A session state. A2AAgent decides whether to send task_id
# for INPUT_REQUIRED or reference_task_ids for task refinement.
next_response = await a2a_agent.run(
next_message,
session=session,
)
```
- Good, because protocol-specific state stays in a protocol-specific type.
- Good, because it aligns with .NET A2A's `A2AAgentSession` shape.
- Good, because Python A2A can still make this pre-release session factory adjustment.
- Good, because `task_state` does not get promoted to a base `AgentSession` concept.
- Bad, because generic consumers cannot read protocol-specific state without knowing about the subclass or a helper API.
- Bad, because it depends on each subclass consistently setting shared session fields such as `service_session_id` where
those are part of the shared abstraction.
### Option B: Extend `service_session_id` with richer service-owned values
Keep the common `service_session_id` case as a plain string. When an agent/service needs more than one service-owned
continuation value, allow `service_session_id` to be a typed structured value, such as a `TypedDict`. The main session ID
used for `gen_ai.conversation.id` should still be extracted by the owning agent, not inferred by generic telemetry code.
Examples:
```python
simple_session = AgentSession(
service_session_id="resp_123",
)
structured_session = AgentSession(
service_session_id=A2AServiceSessionId(
context_id="ctx_123",
task_id="task_789",
task_state=TaskState.TASK_STATE_WORKING,
),
)
```
- Good, because the common case remains a plain string and stays simple.
- Good, because richer service-owned continuation state stays under the existing continuation property.
- Good, because a structured value can make framework-side validation possible before a value is sent back to a service.
- Good, because A2A can keep `context_id`, `task_id`, and `task_state` together as the service/protocol-owned continuation
value without adding A2A fields to base `AgentSession`.
- Neutral, because telemetry needs an agent-owned extractor to pick the `gen_ai.conversation.id` value from either a
string or structured `service_session_id`.
- Neutral, because Python A2A would need a pre-release adjustment to stop relying on `A2AAgentSession` for these fields.
- Bad, because changing the `service_session_id` type is a compatibility risk for users, providers, serialization, and
tests.
- Bad, because every path that sends `service_session_id` back to a service must consistently extract/adapt the
service-owned continuation component.
### Option C: Add a dedicated dict for additional session details
Keep `service_session_id` as the primary opaque service-owned continuation handle, and add a separate dictionary for
additional durable protocol/service values that need to travel with the session.
Example:
```python
session = AgentSession(
service_session_id="ctx_123",
session_details={
"task_id": "task_456",
"task_state": TaskState.TASK_STATE_WORKING,
},
)
```
- Good, because the main service continuation handle stays a plain `service_session_id` string.
- Good, because extra state has an explicit home and does not overload `service_session_id`.
- Good, because generic consumers can look in one documented place for additional session-scoped values.
- Neutral, because helper APIs can hide the raw dictionary access.
- Bad, because this still introduces string-keyed state unless the dict values are wrapped by typed helpers.
- Bad, because it adds another public session field that needs serialization, naming, and compatibility rules.
- Bad, because generic consumers still need to understand the shape or use helpers for the selected agent/session type.
### Option D: Store additional durable state inside `AgentSession.state`
Keep base `AgentSession` unchanged and store additional durable continuation/protocol state under namespaced keys in
`session.state`.
Example:
```python
session = AgentSession(session_id="ctx_123")
session.state["a2a"] = {
"task_id": "task_456",
"task_state": TaskState.TASK_STATE_WORKING,
}
```
- Good, because it avoids new public fields and avoids a subclass requirement.
- Good, because `AgentSession.state` already exists for provider/session state.
- Neutral, because helper APIs can hide the raw dictionary access.
- Bad, because stringly typed state is easier to corrupt and harder to validate.
- Bad, because generic consumers need helper APIs anyway; directly reading nested dictionaries is not a good abstraction.
- Bad, because users may accidentally overwrite or persist invalid protocol state.
## Decision
Chosen decision criteria for the future: **split identity by lifecycle**.
When a protocol emits an id/token, place it by answering "what lifecycle does this value serve?":
- **Future-call continuation state** -> durable session state. Examples: A2A `context_id` + `task_id` + `task_state`;
OpenAI Responses `previous_response_id`/`conversation`.
- **Single-result identity** -> response/message object only. Examples: OpenAI `resp_*`, A2A `message_id`,
A2A `artifact_id`.
- **Resume unfinished work** -> `ContinuationToken` only. Example: a token carrying in-progress task resume data.
- **Run-start-only request fields** -> run method arguments/options, not durable session state. Example: A2A
`reference_task_ids` for a specific follow-up/refinement request.
- **Per-run correlation/telemetry** -> protocol wrapper or run context, not `AgentSession`. Example: wrapper-managed
`run_id` used only for tracing/events.
Durable-state option decision: **Option B: Extend `service_session_id` with richer service-owned values**.
This does **not** add a new top-level identity abstraction; it keeps continuation identity under
`service_session_id` and keeps run correlation in existing run/telemetry context.
The immediate implementation gap is mainly in A2A mapping clarity, but the lifecycle split applies
consistently across providers.
To support telemetry, `BaseAgent` should expose a method that accepts an `AgentSession | None` and returns the value to
use for `gen_ai.conversation.id`. The default implementation should return `session.service_session_id` when it is a
string. Agents that use a structured `service_session_id`, such as `A2AAgent`, should override that method and return the
appropriate primary session/context value.
## Appendix: A2A `task_id` and `reference_task_ids` implementation check
The A2A protocol distinguishes a message's `task_id` from `reference_task_ids`:
- `task_id` associates the message with a specific task.
- `reference_task_ids` provides additional task context, for example when a new task refines or follows up on the result
of a previous task.
The protocol does not appear to prescribe that `task_id` and `reference_task_ids` are mutually exclusive. If both are
present, the natural reading is that the message is associated with one task while also referencing other tasks for
context. The serving agent decides how to interpret that context.
The Python implementation should check and likely adjust the current behavior:
- `task_id` should be updated by the current run when the remote A2A service returns a task/status payload.
- `task_id` should remain durable A2A session state when needed for future calls, for example when a task is
`INPUT_REQUIRED`.
- `reference_task_ids` should be a run parameter / caller intent for the current request, not implicit durable session
continuation state.
- A follow-up/refinement request should pass explicit `reference_task_ids` when it wants to reference previous tasks.
- If both session `task_id` and run `reference_task_ids` are present, the wrapper should preserve the protocol
distinction rather than treating one as a replacement for the other.
- If no `reference_task_ids` are supplied, the wrapper should not automatically infer them from the last session task
unless we deliberately keep that convenience for compatibility.
## Appendix: implementation notes for Option B
The exact names are implementation details, but the shape should be:
```python
class A2AServiceSessionId(TypedDict):
context_id: str
task_id: str | None
task_state: TaskState | None
class AgentSession:
def __init__(
self,
*,
session_id: str | None = None,
service_session_id: str | ServiceSessionId | None = None,
) -> None:
...
class BaseAgent:
def _get_otel_conversation_id(self, session: AgentSession | None) -> str | None:
service_session_id = session.service_session_id if session else None
return service_session_id if isinstance(service_session_id, str) else None
class A2AAgent(BaseAgent):
def _get_otel_conversation_id(self, session: AgentSession | None) -> str | None:
service_session_id = session.service_session_id if session else None
if isinstance(service_session_id, Mapping):
return service_session_id.get("context_id")
return service_session_id if isinstance(service_session_id, str) else None
class AgentTelemetryLayer:
def _trace_agent_invocation(...):
attributes = _get_span_attributes(
...,
thread_id=self._get_otel_conversation_id(session),
...,
)
```
This keeps the OpenTelemetry extraction decision with the agent that owns the service continuation shape. Generic OTel
code should not parse structured `service_session_id` values directly.
`AgentSession` must also be updated so `service_session_id` can store either the current string value or a structured
service-owned value. Serialization must preserve both shapes, and existing serialized sessions with string
`service_session_id` must continue to round-trip unchanged.
## More Information
Related work and issues:
- #4673: native conversation ID propagation.
- #4893: align A2A protocol concepts with Agent Framework session/continuation concepts.
- #2931: Foundry-specific conversation creation helper, split into a separate Python PR.
- #6622: broader provider-agnostic conversation creation API discussion requiring .NET sync.
- [ADR-0015](0015-agent-run-context.md): AgentRunContext for Agent Run.
- [ADR-0018](0018-agentthread-serialization.md): AgentSession serialization.
- [ADR-0026](0026-hosted-session-identity-context.md): hosted session identity context.
+320
View File
@@ -0,0 +1,320 @@
---
status: proposed
contact: eavanvalkenburg
date: 2026-06-11
deciders: eavanvalkenburg
---
# Python hosting core and pluggable channels
## Scope
This specification is the Python implementation plan for [ADR-0027](../decisions/0027-hosting-channels.md). It documents the simplified v1 host/channel contract only.
The v1 contract is:
- `AgentFrameworkHost` owns one Starlette app, one hostable target, and one or more channels.
- A hostable target is either a `SupportsAgentRun`-compatible agent or a `Workflow`.
- Channels contribute routes, middleware, commands, and lifecycle callbacks.
- Channels parse protocol-native input into `ChannelRequest`.
- Channels render their own originating response.
- Session continuity is explicit: a channel supplies `ChannelSession(isolation_key=...)`, and the host resolves/caches an `AgentSession` for that key.
- The host invokes `ChannelRunHook` and `ChannelResponseHook`; channels provide hook configuration and protocol context.
The host does not link identities, route responses to other channels, run background continuations, or multicast in v1. Those enhancements are tracked in [ADR-0028](../decisions/0028-hosting-linking-multicast-enhancements.md).
## Goals
- Let an app expose one agent or workflow on multiple protocols without handwritten Starlette composition.
- Keep protocol parsing and response formatting inside channel packages.
- Provide one session-resolution path shared by all channels.
- Keep the channel authoring surface small enough for new channels to implement.
- Preserve full-fidelity agent and workflow results until a channel decides how to render them.
## Non-goals for v1
The following are removed from the v1 implementation pass:
- `IdentityLinker`, `IdentityAllowlist`, `AuthPolicy`, and `LinkPolicy`
- `ResponseTarget`, active-channel routing, `all_linked`, fan-out, and multicast
- `ChannelPush` and `ChannelPushCodec`
- `DurableTaskRunner`, `InProcessTaskRunner`, and `RetryPolicy`
- continuation tokens and background delivery
- confidentiality tiers
- `agent-framework-hosting-entra`
- `local_identity_link`
These are follow-up design topics, not hidden requirements of the v1 host.
## Packages
| Package | Import surface | Contents |
|---|---|---|
| `agent-framework-hosting` | `agent_framework_hosting` | `AgentFrameworkHost`, channel protocols, key request/result types, hooks, `reset_session`, state-path helpers. |
| `agent-framework-hosting-responses` | `agent_framework_hosting_responses` | `ResponsesChannel`. |
| `agent-framework-hosting-invocations` | `agent_framework_hosting_invocations` | `InvocationsChannel`. |
| `agent-framework-hosting-telegram` | `agent_framework_hosting_telegram` | `TelegramChannel` and Telegram command helpers. |
| `agent-framework-hosting-activity-protocol` | `agent_framework_hosting_activity_protocol` | `ActivityProtocolChannel` for Activity Protocol over Azure Bot Service. |
| `agent-framework-hosting-discord` | `agent_framework_hosting_discord` | `DiscordChannel` and Discord command/interaction helpers. |
| `agent-framework-foundry-hosting` | `agent_framework.foundry_hosting` | Foundry isolation middleware and Foundry-backed hosting helpers usable with the v1 host. |
Channel packages may depend on their native SDKs. The core hosting package should not depend on channel SDKs or on top-level legacy protocol hosts.
## Key Types
### `AgentFrameworkHost`
The host constructor accepts:
- `target`: one `SupportsAgentRun`-compatible object or one `Workflow`
- `channels`: one or more `Channel` instances
- optional Starlette middleware
- optional `state_dir`
- optional workflow `checkpoint_location`
The host exposes:
- `app`: the canonical Starlette ASGI application
- `serve(...)`: a convenience wrapper for local serving
- `reset_session(isolation_key: str)`: rotate the cached `AgentSession` for a host-tracked conversation
`state_dir` is narrowed to v1 host-owned local files only:
- session aliases (`isolation_key` to current `AgentSession` id), and
- workflow checkpoint paths when the app chooses the host-provided file layout.
It is not a store for identity links, continuations, active-channel state, delivery attempts, or multicast payloads.
Externally supplied isolation keys are trusted only after the channel or host middleware has authenticated and authorized the caller. The host uses `isolation_key` as a partition key; the string itself is not proof of identity or ownership.
### `Channel`
A channel implements a small protocol:
- declare a stable channel id/name,
- contribute routes, middleware, commands, and lifecycle callbacks,
- parse inbound protocol data into `ChannelRequest`,
- call the host through `ChannelContext.run(...)` or `ChannelContext.run_stream(...)`, and
- serialize the returned result to the originating protocol response.
Channels own protocol authentication, signature validation, native command registration, and protocol-specific error bodies.
### `ChannelContribution`
`ChannelContribution` is the channel's host-facing contribution:
- Starlette routes and optional middleware,
- native command descriptors,
- startup and shutdown callbacks, and
- any channel-local metadata needed by the package.
The host aggregates contributions but does not interpret protocol payloads.
### `ChannelRequest`
`ChannelRequest` is the host-neutral request envelope produced by a channel. It carries:
- target input,
- optional `ChannelSession`,
- optional `ChannelIdentity`,
- options and attributes produced by the channel, and
- request metadata useful to hooks and context providers.
The host may pass attributes through to context providers and middleware. Channels should treat attributes as a documented extension bag, not as a cross-channel delivery contract.
### `ChannelSession`
`ChannelSession(isolation_key=...)` is the only v1 session-continuity mechanism.
When a request contains an isolation key:
1. The host looks up or creates the cached `AgentSession` for that key.
2. The target runs with that `AgentSession` when the target is an agent.
3. `reset_session(isolation_key)` rotates the alias so the next request starts a new conversation.
If two channels produce the same isolation key on the same host, they share the same cached session. If they produce different keys, they do not share session state.
### `ChannelIdentity`
`ChannelIdentity` is optional request metadata such as channel id, native user id, tenant id, claims, or display attributes.
In v1, `ChannelIdentity` does not link channels, authorize callers, select delivery destinations, or imply that two identities should share an `AgentSession`. A channel that wants shared history must still produce the same `ChannelSession.isolation_key`.
### Hooks
Hooks are optional and channel-owned:
- `ChannelRunHook`: runs after channel parsing and before host invocation; returns the `ChannelRequest` to execute.
- `ChannelResponseHook`: runs after target completion and before the originating channel renders a one-shot response.
- `ChannelStreamUpdateHook`: the host applies it to streamed updates before the originating channel serializes the stream.
Common uses include adapting chat text into workflow inputs, enforcing deployment-specific options, flattening rich output for text-only protocols, or filtering streamed updates for a protocol. Stream update hooks are update-only; they do not automatically sanitize `get_final_response()` output. Channels choose their response transport from the parsed protocol request before invoking run hooks.
### `HostedRunResult`
`HostedRunResult[T]` wraps the target's full-fidelity result plus the resolved `AgentSession | None`.
- Agent targets produce `HostedRunResult[AgentResponse]`.
- Workflow targets produce `HostedRunResult[WorkflowRunResult]`.
The host does not flatten, filter, or translate the result. Each channel decides how much of the result its protocol can carry.
## Host Behavior
1. `AgentFrameworkHost` builds one Starlette app and asks each channel for its contribution.
2. A channel route receives a protocol-native request.
3. The channel validates/parses the native payload and creates `ChannelRequest`.
4. The channel passes the request, optional `ChannelRunHook`, and protocol-native context to the host.
5. The host invokes `ChannelRunHook`, if configured, and receives the prepared request.
6. The host resolves an `AgentSession` from `ChannelSession.isolation_key` when present.
7. The host invokes the agent or workflow target.
8. The host wraps the result in `HostedRunResult` or the streaming equivalent.
9. The host invokes `ChannelResponseHook`, if configured, for non-streaming/final response shaping.
10. The host applies stream update hooks while the channel consumes streams; the channel renders the originating protocol response.
There is no host-level route from one channel's request to another channel's response in v1.
## Workflow Checkpoints
Workflow checkpointing is explicit. Apps either configure checkpoint storage on the workflow itself or pass a `checkpoint_location` to the host so the workflow dispatch path can use the intended file location.
`state_dir` may provide a conventional location for workflow checkpoint files, but checkpointing is still opt-in and separate from agent session history. Checkpoints are workflow-runtime state, not channel state and not identity-link state.
## Foundry Isolation Middleware
V1 keeps Foundry isolation as middleware rather than as a channel-linking feature.
The middleware is installed only when the Foundry hosting environment flag is present. In that environment it reads Foundry-provided isolation values at the trusted hosting boundary, exposes them as read-only request context for Foundry-aware history or memory providers, and rejects unsafe session resumes when the live isolation context does not match persisted session context. Outside Foundry, raw isolation headers are ignored unless an app supplies its own trusted middleware.
This middleware does not create cross-channel identity links and does not authorize non-Foundry channels.
## Current Channels
### Responses
`ResponsesChannel` exposes the OpenAI-compatible Responses API shape. It maps request body fields such as input, options, and conversation identifiers into `ChannelRequest`, and it renders Responses-compatible one-shot or streaming responses.
Responses session continuity uses a channel-selected `isolation_key`, commonly derived from a response/conversation id, caller-provided session id, Foundry isolation context, or deployment-specific request metadata.
### Invocations
`InvocationsChannel` exposes an invocation endpoint for server-side callers and tools. It maps the request body into `ChannelRequest` and renders the invocation result on the same HTTP response.
Invocations is useful for typed workflow inputs because a `ChannelRunHook` can translate the request body into the workflow's expected input type.
### Telegram
`TelegramChannel` supports webhook or polling transport, native command registration, and message rendering back to the originating Telegram chat.
The channel chooses a default `isolation_key` from Telegram-native data such as chat id, user id, or a configured user/chat scope. A `/new` or equivalent command may call `reset_session` for that isolation key.
### Activity Protocol
`ActivityChannel` supports Activity Protocol requests, typically through Azure Bot Service for Teams, Web Chat, and other Bot Framework-fronted surfaces.
The channel maps incoming `Activity` objects to `ChannelRequest` and renders a reply activity to the originating conversation. Proactive Activity delivery, active-channel routing, and all-linked fan-out are not v1 host semantics.
### Discord
`DiscordChannel` supports Discord messages, slash commands, and interactions as channel-native input.
The channel maps Discord-native user, guild, channel, thread, and interaction data into `ChannelRequest` metadata and a configured `ChannelSession.isolation_key`. It renders the result to the originating Discord response path.
## High-level Samples
### One agent on Responses
```python
host = AgentFrameworkHost(
target=agent,
channels=[ResponsesChannel()],
)
app = host.app
```
### One agent on multiple channels
```python
host = AgentFrameworkHost(
target=agent,
channels=[
ResponsesChannel(),
InvocationsChannel(),
TelegramChannel(bot_token=os.environ["TELEGRAM_BOT_TOKEN"]),
],
)
host.serve(host="localhost", port=8000)
```
The host owns one Starlette app. Each channel contributes its own routes and renders its own response.
### Adapting a request before execution
```python
from dataclasses import replace
def enforce_options(request: ChannelRequest) -> ChannelRequest:
options = dict(request.options or {})
options["temperature"] = 0
return replace(request, options=options)
host = AgentFrameworkHost(
target=agent,
channels=[ResponsesChannel(run_hook=enforce_options)],
)
```
### Workflow with explicit checkpoints
```python
host = AgentFrameworkHost(
target=workflow,
channels=[InvocationsChannel(run_hook=adapt_to_workflow_input)],
checkpoint_location=Path("./.af-hosting/workflow_checkpoints"),
)
```
The hook adapts channel-native input to the workflow's typed input. Checkpoints use the explicit workflow checkpoint location, not identity-link or delivery storage.
### Message channel reset command
```python
async def new_chat(context):
if context.request.session is not None:
await context.host.reset_session(context.request.session.isolation_key)
await context.reply("Started a new conversation.")
```
Telegram, Activity Protocol, and Discord can expose equivalent native commands when their protocols support them.
## Follow-up Enhancements
See [ADR-0028](../decisions/0028-hosting-linking-multicast-enhancements.md) for the deferred design covering:
- cross-channel identity linking,
- authorization and allowlists,
- non-originating response delivery,
- active-channel routing,
- multicast and all-linked delivery,
- background runs and continuation tokens,
- durable delivery runners,
- retry/replay semantics, and
- payload serialization.
Those enhancements must layer on top of this v1 contract without requiring v1 users to adopt them.
## Validation Gates
The Python implementation should be considered complete when:
- a sample uses one `AgentFrameworkHost` with multiple channels and no manual Starlette route composition,
- each current channel has contract tests for route contribution, lifecycle, request parsing, hooks, and originating response rendering,
- session tests prove shared `isolation_key` values share an `AgentSession` and `reset_session` rotates it,
- workflow tests or samples use explicit `checkpoint_location`,
- Foundry isolation middleware is covered by integration or contract tests,
- no v1 package exposes the removed linking, multicast, durable-runner, or continuation APIs, and
- this spec and ADR-0027 remain aligned.
-1
View File
@@ -1 +0,0 @@
../../../.github/skills/pull-requests
+116
View File
@@ -0,0 +1,116 @@
---
name: pull-requests
description: >
Guidance for creating pull requests and handling PR review comments in the
Agent Framework repository. Use this when writing a PR description (filling out
the PR template) or when responding to and resolving review comments on an
existing PR.
---
# Pull Request Workflow
This skill covers two tasks: (1) writing a high-quality PR description, and
(2) handling review comments on an existing PR.
## 1. Writing the PR description
Always follow the repository PR template at
[`.github/pull_request_template.md`](../../../../.github/pull_request_template.md). Keep its
exact structure and headings. Fill every section:
### `### Motivation & Context`
Explain *why* the change is needed: the problem it solves and the scenario it
contributes to. Describe the net change relative to `main` — this is implied, so
do **not** spell out "vs main" explicitly.
### `### Description & Review Guide`
Describe the changes, the overall approach, and the design. Answer the three
prompts:
- **What are the major changes?**
- **What is the impact of these changes?**
- **What do you want reviewers to focus on?** — This item is for **human
reviewers only**. Automated/AI reviewers must ignore it and review the entire
change rather than narrowing scope to it.
### `### Related Issue`
Link the issue the PR fixes using a GitHub closing keyword (`Fixes #123` /
`Closes #123`) so it closes automatically on merge. A PR with no linked issue may
be closed regardless of how valid the change is. Before opening, confirm there is
no other open PR for the same issue; if there is, explain how this PR differs.
### `### Contribution Checklist`
Check every item that applies. For the breaking-change item:
- Leave **"This is not a breaking change."** checked for the common case.
- If the change **is** breaking, add the `breaking change` label **or** put
`[BREAKING]` in the title prefix, before or after a language prefix such as
`Python:` or `.NET:` — workflows keep the label and the title prefix in sync
automatically (see `.github/workflows/label-title-prefix.yml` and
`.github/workflows/label-pr.yml`).
### Do not
- Do **not** add ad-hoc sections such as "Validation" or "Tests run"; CI/CD and
the checklist already cover validation status.
- Do **not** remove or reorder the template's headings.
### Creating the PR
Open new PRs as **drafts** until they are ready for review. Example:
```bash
gh pr create --repo microsoft/agent-framework --base main \
--head <your-fork-owner>:<branch> --draft \
--title "<concise title>" --body "<body following the template>"
```
## 2. Handling review comments
When a PR receives review comments, follow this sequence — **do not start editing
code before the user has reviewed the plan**:
1. **Review the comments.** Read every review comment and thread on the PR,
including inline code comments and general review summaries.
2. **Make a plan.** Produce a concrete plan describing how each comment will be
addressed (or why it should not be, with reasoning).
3. **Let the user review the plan.** Present the plan and wait for the user's
approval or adjustments before implementing anything.
4. **Implement.** Make the agreed changes.
5. **Reply to every comment.** Add a reply to **all** comments explaining how it
was addressed (or the agreed outcome) — leave none unanswered.
6. **Resolve resolved threads.** Mark a review thread as resolved only when the
comment has actually been addressed.
### Useful commands
List review comments and threads:
```bash
# Inline review comments
gh api repos/{owner}/{repo}/pulls/{pr}/comments
# Review threads with resolution state (GraphQL)
gh api graphql -f query='
query($owner:String!,$repo:String!,$pr:Int!){
repository(owner:$owner,name:$repo){
pullRequest(number:$pr){
reviewThreads(first:100){
nodes{ id isResolved comments(first:50){ nodes{ id body author{login} } } }
}
}
}
}' -F owner={owner} -F repo={repo} -F pr={pr}
```
Reply to an inline review comment:
```bash
gh api repos/{owner}/{repo}/pulls/{pr}/comments/{comment_id}/replies \
-f body="Addressed in <commit>: <explanation>"
```
Resolve a review thread (needs the thread node id from the GraphQL query above):
```bash
gh api graphql -f query='
mutation($threadId:ID!){
resolveReviewThread(input:{threadId:$threadId}){ thread{ isResolved } }
}' -F threadId={thread_id}
```
+4 -4
View File
@@ -12,7 +12,7 @@
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="12.20.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.5.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.6.0" />
<PackageVersion Include="Aspire.Hosting" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
<PackageVersion Include="Aspire.Azure.AI.Inference" Version="13.1.0-preview.1.25616.3" />
@@ -27,10 +27,10 @@
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.4" />
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.5" />
<PackageVersion Include="Azure.Search.Documents" Version="12.0.0" />
<PackageVersion Include="Azure.AI.Projects" Version="2.1.0-beta.2" />
<PackageVersion Include="Azure.AI.Projects" Version="2.1.0-beta.3" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
<PackageVersion Include="Azure.Core" Version="1.56.0" />
<PackageVersion Include="Azure.Core" Version="1.57.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" />
@@ -45,7 +45,7 @@
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.8" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.5" />
<PackageVersion Include="System.ClientModel" Version="1.12.0" />
<PackageVersion Include="System.ClientModel" Version="1.13.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" />
+16 -7
View File
@@ -1,4 +1,4 @@
<Solution>
<Solution>
<Configurations>
<BuildType Name="Debug" />
<BuildType Name="Publish" />
@@ -117,9 +117,11 @@
<Project Path="samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step06_McpBasedSkills/Agent_Step06_McpBasedSkills.csproj" />
<Project Path="samples/02-agents/AgentSkills/Agent_Step07_SkillsAutoApproval/Agent_Step07_SkillsAutoApproval.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/Harness/">
<File Path="samples/02-agents/Harness/README.md" />
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step01_MeetYourClaw/Claw_Step01_MeetYourClaw.csproj" />
<Project Path="samples/02-agents/Harness/ConsoleReactiveComponents/ConsoleReactiveComponents.csproj" />
<Project Path="samples/02-agents/Harness/ConsoleReactiveFramework/ConsoleReactiveFramework.csproj" />
<Project Path="samples/02-agents/Harness/Harness_Shared_Console/Harness_Shared_Console.csproj" />
@@ -192,10 +194,10 @@
<File Path="samples/02-agents/AgentWithMemory/README.md" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step01_ChatHistoryMemory/AgentWithMemory_Step01_ChatHistoryMemory.csproj" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step02_MemoryUsingMem0/AgentWithMemory_Step02_MemoryUsingMem0.csproj" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step05_BoundedChatHistory/AgentWithMemory_Step05_BoundedChatHistory.csproj" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey/AgentWithMemory_Step03_MemoryUsingValkey.csproj" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock.csproj" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step05_BoundedChatHistory/AgentWithMemory_Step05_BoundedChatHistory.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AgentProviders/openai/">
<File Path="samples/02-agents/AgentProviders/openai/README.md" />
@@ -217,6 +219,7 @@
<Folder Name="/Samples/02-agents/ModelContextProtocol/">
<File Path="samples/02-agents/ModelContextProtocol/README.md" />
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_LongRunningTask_Client/Agent_MCP_LongRunningTask_Client.csproj" />
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_PerRun_AuthHeaders/Agent_MCP_PerRun_AuthHeaders.csproj" />
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_Server/Agent_MCP_Server.csproj" />
<Project Path="samples/02-agents/ModelContextProtocol/Agent_MCP_Server_Auth/Agent_MCP_Server_Auth.csproj" />
<Project Path="samples/02-agents/ModelContextProtocol/FoundryAgent_Hosted_MCP/FoundryAgent_Hosted_MCP.csproj" />
@@ -239,10 +242,10 @@
</Folder>
<Folder Name="/Samples/03-workflows/Declarative/">
<File Path="samples/03-workflows/Declarative/README.md" />
<Project Path="samples/03-workflows/Declarative/AotCheckpointing/AotCheckpointing.csproj" />
<Project Path="samples/03-workflows/Declarative/ConfirmInput/ConfirmInput.csproj" />
<Project Path="samples/03-workflows/Declarative/CustomerSupport/CustomerSupport.csproj" />
<Project Path="samples/03-workflows/Declarative/DeepResearch/DeepResearch.csproj" />
<Project Path="samples/03-workflows/Declarative/ExecuteCode/ExecuteCode.csproj" />
<Project Path="samples/03-workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj" />
<Project Path="samples/03-workflows/Declarative/FunctionTools/FunctionTools.csproj" />
<Project Path="samples/03-workflows/Declarative/HostedWorkflow/HostedWorkflow.csproj" />
@@ -331,6 +334,9 @@
<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-LocalCodeAct/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/HostedLocalCodeAct.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/HostedMcpTools.csproj" />
</Folder>
@@ -408,6 +414,7 @@
<Folder Name="/Samples/05-end-to-end/Evaluation/">
<Project Path="samples/05-end-to-end/Evaluation/Evaluation_ConversationSplits/Evaluation_ConversationSplits.csproj" />
<Project Path="samples/05-end-to-end/Evaluation/Evaluation_FoundryQuality/Evaluation_FoundryQuality.csproj" />
<Project Path="samples/05-end-to-end/Evaluation/Evaluation_FoundryRubric/Evaluation_FoundryRubric.csproj" />
<Project Path="samples/05-end-to-end/Evaluation/Evaluation_MixedProviders/Evaluation_MixedProviders.csproj" />
</Folder>
<Folder Name="/Samples/05-end-to-end/A2AClientServer/">
@@ -611,23 +618,24 @@
<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" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AspNetCore/Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AspNetCore/Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hyperlight/Microsoft.Agents.AI.Hyperlight.csproj" />
<Project Path="src/Microsoft.Agents.AI.LocalCodeAct/Microsoft.Agents.AI.LocalCodeAct.csproj" />
<Project Path="src/Microsoft.Agents.AI.Mcp/Microsoft.Agents.AI.Mcp.csproj" />
<Project Path="src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj" />
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Purview/Microsoft.Agents.AI.Purview.csproj" />
<Project Path="src/Microsoft.Agents.AI.Tools.Shell/Microsoft.Agents.AI.Tools.Shell.csproj" />
<Project Path="src/Microsoft.Agents.AI.Valkey/Microsoft.Agents.AI.Valkey.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Declarative/Microsoft.Agents.AI.Workflows.Declarative.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows.Generators/Microsoft.Agents.AI.Workflows.Generators.csproj" />
<Project Path="src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj" />
<Project Path="src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Valkey/Microsoft.Agents.AI.Valkey.csproj" />
</Folder>
<Folder Name="/Tests/" />
<Folder Name="/Tests/IntegrationTests/">
@@ -671,17 +679,18 @@
<Project Path="tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.UnitTests/Microsoft.Agents.AI.Hosting.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hyperlight.UnitTests/Microsoft.Agents.AI.Hyperlight.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.LocalCodeAct.UnitTests/Microsoft.Agents.AI.LocalCodeAct.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Mcp.UnitTests/Microsoft.Agents.AI.Mcp.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Mem0.UnitTests/Microsoft.Agents.AI.Mem0.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.OpenAI.UnitTests/Microsoft.Agents.AI.OpenAI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Purview.UnitTests/Microsoft.Agents.AI.Purview.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Tools.Shell.UnitTests/Microsoft.Agents.AI.Tools.Shell.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.UnitTests/Microsoft.Agents.AI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Valkey.UnitTests/Microsoft.Agents.AI.Valkey.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.Generators.UnitTests/Microsoft.Agents.AI.Workflows.Generators.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Workflows.UnitTests/Microsoft.Agents.AI.Workflows.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Valkey.UnitTests/Microsoft.Agents.AI.Valkey.UnitTests.csproj" />
</Folder>
</Solution>
+10 -10
View File
@@ -26,8 +26,8 @@ internal static class GetStartedSamples
{
Name = "01_hello_agent",
ProjectPath = "samples/01-get-started/01_hello_agent",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
ExpectedOutputDescription =
[
"The output should contain a joke about a pirate.",
@@ -40,8 +40,8 @@ internal static class GetStartedSamples
{
Name = "02_add_tools",
ProjectPath = "samples/01-get-started/02_add_tools",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
MustContain = [],
ExpectedOutputDescription =
[
@@ -56,8 +56,8 @@ internal static class GetStartedSamples
{
Name = "03_multi_turn",
ProjectPath = "samples/01-get-started/03_multi_turn",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
ExpectedOutputDescription =
[
"The output should contain a joke about a pirate.",
@@ -71,8 +71,8 @@ internal static class GetStartedSamples
{
Name = "04_memory",
ProjectPath = "samples/01-get-started/04_memory",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
MustContain =
[
">> Use session with blank memory",
@@ -97,8 +97,8 @@ internal static class GetStartedSamples
{
Name = "06_host_your_agent",
ProjectPath = "samples/01-get-started/06_host_your_agent",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
SkipReason = "Requires Azure Functions Core Tools runtime and starts a web server.",
},
];
+33 -11
View File
@@ -18,13 +18,14 @@
// Note: By default, this tool expects sample build outputs to already exist.
// Pre-build the solution before running, or pass --build to avoid missing build output failures.
//
// Required environment variables (for AI-powered samples):
// AZURE_OPENAI_ENDPOINT
// AZURE_OPENAI_DEPLOYMENT_NAME (optional, defaults to gpt-5-mini)
// Required environment variables (for AI-powered verification):
// FOUNDRY_PROJECT_ENDPOINT — Your Azure AI Foundry project endpoint
// FOUNDRY_MODEL — Model deployment name (optional, defaults to gpt-5.4-mini)
using System.Diagnostics;
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using VerifySamples;
var options = VerifyOptions.Parse(args);
@@ -43,14 +44,33 @@ if (!File.Exists(Path.Combine(dotnetRoot, "agent-framework-dotnet.slnx")))
}
// Set up the AI verifier
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5-mini";
var foundryEndpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT");
var foundryModel = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
OpenAI.Chat.ChatClient? chatClient = null;
if (!string.IsNullOrEmpty(endpoint))
AIAgent? verifierAgent = null;
if (!string.IsNullOrEmpty(foundryEndpoint))
{
chatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetChatClient(deploymentName);
verifierAgent = new AIProjectClient(new Uri(foundryEndpoint), new DefaultAzureCredential())
.AsAIAgent(
model: foundryModel,
instructions: """
You are a test output verifier. You will be given:
1. The actual stdout output of a program
2. The stderr output (if any)
3. A list of expectations about what the output should contain or demonstrate
Your job is to determine whether the actual output satisfies each expectation.
Be reasonable the output comes from an LLM so exact wording won't match, but the
semantic intent should be clearly satisfied.
In your response, you MUST:
- Always provide ai_reasoning with a brief overall assessment.
- Always provide exactly one entry in expectation_results for each expectation,
in the same order as the input list.
- For each expectation_results entry, echo the expectation text in the expectation
field and explain your assessment in the detail field, citing evidence from the output.
""",
name: "OutputVerifier");
}
// Set up optional log file writer
@@ -61,11 +81,13 @@ if (options.LogFilePath is not null)
await logWriter.WriteHeaderAsync();
}
Console.WriteLine($"Foundry endpoint: {foundryEndpoint ?? "(not set AI verification disabled)"}, Model: {foundryModel}");
try
{
// Run all samples
var reporter = new ConsoleReporter();
var verifier = new SampleVerifier(chatClient);
var verifier = new SampleVerifier(verifierAgent);
var orchestrator = new VerificationOrchestrator(verifier, reporter, dotnetRoot, TimeSpan.FromMinutes(3), logWriter, buildSamples: options.BuildSamples);
var run = await orchestrator.RunAllAsync(options.Samples, options.MaxParallelism);
+3 -26
View File
@@ -3,8 +3,6 @@
using System.ComponentModel;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
namespace VerifySamples;
@@ -17,33 +15,12 @@ internal sealed class SampleVerifier
private readonly AIAgent? _verifierAgent;
/// <summary>
/// Creates a verifier. If <paramref name="chatClient"/> is provided,
/// Creates a verifier. If <paramref name="verifierAgent"/> is provided,
/// AI-based verification is available for non-deterministic samples.
/// </summary>
public SampleVerifier(ChatClient? chatClient = null)
public SampleVerifier(AIAgent? verifierAgent = null)
{
if (chatClient is not null)
{
this._verifierAgent = chatClient.AsAIAgent(
instructions: """
You are a test output verifier. You will be given:
1. The actual stdout output of a program
2. The stderr output (if any)
3. A list of expectations about what the output should contain or demonstrate
Your job is to determine whether the actual output satisfies each expectation.
Be reasonable the output comes from an LLM so exact wording won't match, but the
semantic intent should be clearly satisfied.
In your response, you MUST:
- Always provide ai_reasoning with a brief overall assessment.
- Always provide exactly one entry in expectation_results for each expectation,
in the same order as the input list.
- For each expectation_results entry, echo the expectation text in the expectation
field and explain your assessment in the detail field, citing evidence from the output.
""",
name: "OutputVerifier");
}
this._verifierAgent = verifierAgent;
}
/// <summary>
+49 -59
View File
@@ -30,8 +30,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_StartHere_02_AgentsInWorkflows",
ProjectPath = "samples/03-workflows/_StartHere/02_AgentsInWorkflows",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
ExpectedOutputDescription =
[
"The output should show agent responses from a translation workflow.",
@@ -43,8 +43,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_StartHere_03_AgentWorkflowPatterns",
ProjectPath = "samples/03-workflows/_StartHere/03_AgentWorkflowPatterns",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
Inputs = ["sequential"],
InputDelayMs = 3000,
ExpectedOutputDescription =
@@ -81,8 +81,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_StartHere_06_MixedWorkflowAgentsAndExecutors",
ProjectPath = "samples/03-workflows/_StartHere/06_MixedWorkflowAgentsAndExecutors",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
Inputs = ["What is 2 plus 2?"],
InputDelayMs = 3000,
ExpectedOutputDescription =
@@ -96,8 +96,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_StartHere_07_WriterCriticWorkflow",
ProjectPath = "samples/03-workflows/_StartHere/07_WriterCriticWorkflow",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
MustContain = ["=== Writer-Critic Iteration Workflow ==="],
ExpectedOutputDescription =
[
@@ -115,8 +115,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_Agents_CustomAgentExecutors",
ProjectPath = "samples/03-workflows/Agents/CustomAgentExecutors",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
ExpectedOutputDescription =
[
"The output should show custom workflow events including slogan generation and feedback.",
@@ -128,8 +128,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_Agents_FoundryAgent",
ProjectPath = "samples/03-workflows/Agents/FoundryAgent",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
SkipReason = "Requires Azure AI Foundry project endpoint.",
},
@@ -137,8 +137,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_Agents_GroupChatToolApproval",
ProjectPath = "samples/03-workflows/Agents/GroupChatToolApproval",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
MustContain = ["Starting group chat workflow for software deployment..."],
ExpectedOutputDescription =
[
@@ -153,8 +153,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_Agents_WorkflowAsAnAgent",
ProjectPath = "samples/03-workflows/Agents/WorkflowAsAnAgent",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
Inputs = ["hello", "exit"],
InputDelayMs = 5000,
ExpectedOutputDescription =
@@ -219,8 +219,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_Concurrent_Concurrent",
ProjectPath = "samples/03-workflows/Concurrent/Concurrent",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
ExpectedOutputDescription =
[
"The output should show results from concurrent agent processing.",
@@ -247,8 +247,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_ConditionalEdges_01_EdgeCondition",
ProjectPath = "samples/03-workflows/ConditionalEdges/01_EdgeCondition",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
ExpectedOutputDescription =
[
"The output should show an email being classified as spam or not spam and processed accordingly.",
@@ -260,8 +260,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_ConditionalEdges_02_SwitchCase",
ProjectPath = "samples/03-workflows/ConditionalEdges/02_SwitchCase",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
ExpectedOutputDescription =
[
"The output should show an ambiguous email being classified as spam, not spam, or uncertain.",
@@ -273,8 +273,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_ConditionalEdges_03_MultiSelection",
ProjectPath = "samples/03-workflows/ConditionalEdges/03_MultiSelection",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
ExpectedOutputDescription =
[
"The output should show an email being classified and potentially routed to multiple handlers.",
@@ -371,8 +371,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_Observability_WorkflowAsAnAgent",
ProjectPath = "samples/03-workflows/Observability/WorkflowAsAnAgent",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
SkipReason = "Interactive console with ReadLine loop; requires OTLP endpoint.",
},
@@ -384,7 +384,7 @@ internal static class WorkflowSamples
{
Name = "Workflow_Declarative_ConfirmInput",
ProjectPath = "samples/03-workflows/Declarative/ConfirmInput",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
Inputs = ["hello", "hello"],
InputDelayMs = 8000,
ExpectedOutputDescription = ["The output should show a confirmation prompt and a user response."],
@@ -394,8 +394,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_Declarative_CustomerSupport",
ProjectPath = "samples/03-workflows/Declarative/CustomerSupport",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
Inputs = ["My laptop won't start"],
InputDelayMs = 3000,
ExpectedOutputDescription = ["The output should show a customer support workflow processing a laptop issue, with agent responses providing troubleshooting or support."],
@@ -405,26 +405,16 @@ internal static class WorkflowSamples
{
Name = "Workflow_Declarative_DeepResearch",
ProjectPath = "samples/03-workflows/Declarative/DeepResearch",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
SkipReason = "Requires external weather API (wttr.in).",
},
new SampleDefinition
{
Name = "Workflow_Declarative_ExecuteCode",
ProjectPath = "samples/03-workflows/Declarative/ExecuteCode",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
Inputs = ["What is 12 * 34?"],
InputDelayMs = 5000,
ExpectedOutputDescription = ["The output should show a declarative workflow executing generated code, processing a math question and producing a result."],
},
new SampleDefinition
{
Name = "Workflow_Declarative_ExecuteWorkflow",
ProjectPath = "samples/03-workflows/Declarative/ExecuteWorkflow",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
SkipReason = "Requires a workflow file path as a CLI argument.",
},
@@ -432,8 +422,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_Declarative_FunctionTools",
ProjectPath = "samples/03-workflows/Declarative/FunctionTools",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
Inputs = ["What are today's specials?", "EXIT"],
InputDelayMs = 8000,
ExpectedOutputDescription = ["The output should show a workflow calling function tools (e.g. a menu plugin) to answer a question about restaurant specials."],
@@ -443,7 +433,7 @@ internal static class WorkflowSamples
{
Name = "Workflow_Declarative_HostedWorkflow",
ProjectPath = "samples/03-workflows/Declarative/HostedWorkflow",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
SkipReason = "Hosts a persistent workflow server that does not exit.",
},
@@ -451,8 +441,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_Declarative_InputArguments",
ProjectPath = "samples/03-workflows/Declarative/InputArguments",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
Inputs = ["I'd like to visit Seattle", "EXIT"],
InputDelayMs = 8000,
ExpectedOutputDescription = ["The output should show a workflow capturing location input and providing travel-related information about Seattle."],
@@ -462,8 +452,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_Declarative_InvokeFunctionTool",
ProjectPath = "samples/03-workflows/Declarative/InvokeFunctionTool",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
Inputs = ["What's the soup of the day?", "EXIT"],
InputDelayMs = 8000,
ExpectedOutputDescription = ["The output should show a workflow invoking a function tool (e.g. a menu plugin) to answer a question about the soup of the day."],
@@ -473,8 +463,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_Declarative_InvokeFoundryToolboxMcp",
ProjectPath = "samples/03-workflows/Declarative/InvokeFoundryToolboxMcp",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME", "FOUNDRY_TOOLBOX_NAME", "FOUNDRY_AGENT_TOOLSET_API_VERSION"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL", "FOUNDRY_TOOLBOX_NAME", "FOUNDRY_AGENT_TOOLSET_API_VERSION"],
Inputs = ["How do I use Azure OpenAI with my data?"],
InputDelayMs = 3000,
ExpectedOutputDescription = ["The output should show a workflow using Foundry Toolbox MCP tools to search Microsoft Learn documentation and web search to provide a summary of results."],
@@ -484,8 +474,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_Declarative_InvokeMcpTool",
ProjectPath = "samples/03-workflows/Declarative/InvokeMcpTool",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
Inputs = ["Search for .NET tutorials on Microsoft Learn"],
InputDelayMs = 3000,
ExpectedOutputDescription = ["The output should show a workflow using MCP tools to search Microsoft Learn documentation and provide a summary of results."],
@@ -495,8 +485,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_Declarative_Marketing",
ProjectPath = "samples/03-workflows/Declarative/Marketing",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
Inputs = ["A smart water bottle that tracks hydration"],
InputDelayMs = 3000,
ExpectedOutputDescription = ["The output should show a marketing workflow generating content about a smart water bottle product."],
@@ -506,8 +496,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_Declarative_StudentTeacher",
ProjectPath = "samples/03-workflows/Declarative/StudentTeacher",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
Inputs = ["What is 18 + 27?"],
InputDelayMs = 3000,
ExpectedOutputDescription = ["The output should show a student-teacher workflow where a student asks a math question and a teacher provides the answer."],
@@ -517,8 +507,8 @@ internal static class WorkflowSamples
{
Name = "Workflow_Declarative_ToolApproval",
ProjectPath = "samples/03-workflows/Declarative/ToolApproval",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
Inputs = ["Search for .NET tutorials", "EXIT"],
InputDelayMs = 8000,
ExpectedOutputDescription = ["The output should show a workflow using an MCP tool with approval to search Microsoft Learn, followed by an exit from the input loop."],
@@ -12,13 +12,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.200",
"version": "10.0.301",
"rollForward": "minor",
"allowPrerelease": false
},
+3 -3
View File
@@ -1,14 +1,14 @@
<Project>
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.10.0</VersionPrefix>
<VersionPrefix>1.11.1</VersionPrefix>
<RCNumber>1</RCNumber>
<DateSuffix>260610</DateSuffix>
<DateSuffix>260625</DateSuffix>
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</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.10.0</GitTag>
<GitTag>1.11.1</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
@@ -9,13 +9,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -1,23 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create and use a simple AI agent with Azure OpenAI as the backend.
// This sample shows how to create and use a simple AI agent with AIProjectClient as the backend.
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var 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.
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName)
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(model: model, instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent and output the text result.
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate."));
@@ -9,13 +9,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -1,31 +1,27 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use a ChatClientAgent with function tools.
// It shows both non-streaming and streaming agent interactions using menu-related tools.
// This sample demonstrates how to use an AIProjectClient agent with function tools.
// It shows both non-streaming and streaming agent interactions using weather tools.
using System.ComponentModel;
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var model = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
[Description("Get the weather for a given location.")]
static string GetWeather([Description("The location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";
// Create the chat client and agent, and provide the function tool to the agent.
// Create the agent and provide the function tool to the agent.
// 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)
.AsAIAgent(instructions: "You are a helpful assistant", tools: [AIFunctionFactory.Create(GetWeather)]);
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(model: model, instructions: "You are a helpful assistant", tools: [AIFunctionFactory.Create(GetWeather)]);
// Non-streaming agent interaction with function tools.
Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?"));
@@ -9,13 +9,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -2,22 +2,18 @@
// This sample shows how to create and use a simple AI agent with a multi-turn conversation.
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var 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.
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName)
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(model: model, instructions: "You are good at telling jokes.", name: "Joker");
// Invoke the agent with a multi-turn conversation, where the context is preserved in the session object.
AgentSession session = await agent.CreateSessionAsync();
@@ -9,13 +9,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -8,23 +8,26 @@
using System.Text;
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
using SampleApp;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var 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.
ChatClient chatClient = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName);
var projectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
// Get the underlying IChatClient to use for the memory component.
// The memory provider needs direct IChatClient access for structured extraction.
IChatClient chatClient = projectClient
.AsAIAgent(model: model, instructions: "You are a friendly assistant.Always address the user by their name.")
.GetService<IChatClient>()
?? throw new InvalidOperationException("Could not retrieve IChatClient from AIProjectClient agent.");
// Create the agent and provide a factory to add our custom memory component to
// all sessions created by the agent. Here each new memory component will have its own
@@ -35,8 +38,7 @@ ChatClient chatClient = new AzureOpenAIClient(
// and its storage to that user id.
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions()
{
ChatOptions = new() { Instructions = "You are a friendly assistant. Always address the user by their name." },
AIContextProviders = [new UserInfoMemory(chatClient.AsIChatClient())]
AIContextProviders = [new UserInfoMemory(chatClient)]
});
// Create a new session for the conversation.
@@ -21,11 +21,10 @@
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -4,36 +4,32 @@
//
// Prerequisites:
// - Azure Functions Core Tools
// - Azure OpenAI resource
// - Foundry project endpoint and credentials
//
// Environment variables:
// AZURE_OPENAI_ENDPOINT
// AZURE_OPENAI_DEPLOYMENT_NAME (defaults to "gpt-5.4-mini")
// FOUNDRY_PROJECT_ENDPOINT
// FOUNDRY_MODEL (defaults to "gpt-5.4-mini")
//
// Run with: func start
// Then call: POST http://localhost:7071/api/agents/HostedAgent/run
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var model = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// Set up an AI agent following the standard Microsoft Agent Framework pattern.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// 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)
.AsAIAgent(
instructions: "You are a helpful assistant hosted in Azure Functions.",
name: "HostedAgent");
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(model: model, instructions: "You are a helpful assistant hosted in Azure Functions.", name: "HostedAgent");
// Configure the function app to host the AI agent.
// This will automatically generate HTTP API endpoints for the agent.
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,13 +9,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="OpenAI" />
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="OpenTelemetry.Exporter.Console" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
@@ -25,7 +22,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>
@@ -3,7 +3,7 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Azure.Monitor.OpenTelemetry.Exporter;
using Microsoft.Agents.AI;
@@ -96,8 +96,8 @@ Console.WriteLine("""
Type your message and press Enter. Type 'exit' or empty message to quit.
""");
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT environment variable is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT environment variable is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// Log application startup
appLogger.LogInformation("OpenTelemetry Aspire Demo application started");
@@ -112,20 +112,19 @@ static async Task<string> GetWeatherAsync([Description("The location to get the
// 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.
using var instrumentedChatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetChatClient(deploymentName)
.AsIChatClient() // Converts a native OpenAI SDK ChatClient into a Microsoft.Extensions.AI.IChatClient
.AsBuilder()
.UseFunctionInvocation()
.UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the chat client level
.Build();
appLogger.LogInformation("Creating Agent with OpenTelemetry instrumentation");
// Create the agent with the instrumented chat client
var agent = new ChatClientAgent(instrumentedChatClient,
name: "OpenTelemetryDemoAgent",
instructions: "You are a helpful assistant that provides concise and informative responses.",
tools: [AIFunctionFactory.Create(GetWeatherAsync)])
var agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(
model: deploymentName,
instructions: "You are a helpful assistant that provides concise and informative responses.",
name: "OpenTelemetryDemoAgent",
tools: [AIFunctionFactory.Create(GetWeatherAsync)],
clientFactory: client => client
.AsBuilder()
.UseFunctionInvocation()
.UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the chat client level
.Build())
.AsBuilder()
.UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
.Build();
@@ -1,6 +1,6 @@
# OpenTelemetry Aspire Demo with Azure OpenAI
# OpenTelemetry Aspire Demo with Microsoft Foundry
This demo showcases the integration of OpenTelemetry with the Microsoft Agent Framework using Azure OpenAI and .NET Aspire Dashboard for telemetry visualization.
This demo showcases the integration of OpenTelemetry with the Microsoft Agent Framework using Microsoft Foundry and the .NET Aspire Dashboard for telemetry visualization.
## Overview
@@ -15,7 +15,7 @@ The demo consists of three main components:
```mermaid
graph TD
A["Console App<br/>(Interactive)"] --> B["Agent Framework<br/>with OpenTel<br/>Instrumentation"]
B --> C["Azure OpenAI<br/>Service"]
B --> C["Microsoft Foundry<br/>Project"]
A --> D["Aspire Dashboard<br/>(OpenTelemetry Visualization)"]
B --> D
```
@@ -23,21 +23,21 @@ graph TD
## Prerequisites
- .NET 10 SDK or later
- Azure OpenAI service endpoint and deployment configured
- Microsoft Foundry project endpoint and model configured
- Azure CLI installed and authenticated (for Azure credential authentication)
- Docker installed (for running Aspire Dashboard)
- [Optional] Application Insights and Grafana
## Configuration
### Azure OpenAI Setup
### Microsoft Foundry Setup
Set the following 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
$env:FOUNDRY_PROJECT_ENDPOINT="https://<your-project>.services.ai.azure.com/api/projects/<your-project>"
$env:FOUNDRY_MODEL="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
```
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource.
**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Foundry project.
### [Optional] Application Insights Setup
Set the following environment variables:
@@ -56,7 +56,7 @@ The easiest way to run the demo is using the provided PowerShell script:
```
This script will automatically:
- ✅ Check prerequisites (Docker, Azure OpenAI configuration)
- ✅ Check prerequisites (Docker, Foundry configuration)
- 🔨 Build the console application
- 🐳 Start the Aspire Dashboard via Docker (with anonymous access)
- ⏳ Wait for dashboard to be ready (polls port until listening)
@@ -124,7 +124,7 @@ You:
3. Each trace contains:
- An outer span for the entire agent interaction
- Inner spans from the Agent Framework's OpenTelemetry instrumentation
- Spans from HTTP calls to Azure OpenAI
- Spans from HTTP calls to Microsoft Foundry
### Metrics
1. Navigate to the **Metrics** tab
@@ -158,7 +158,7 @@ Open dashboard in Azure portal: <https://aka.ms/amg/dash/af-workflow>
- **Telemetry correlation** across the entire request flow
### Agent Framework Features
- **ChatClientAgent** with Azure OpenAI integration
- **ChatClientAgent** created from `AIProjectClient`
- **OpenTelemetry wrapper** using `.WithOpenTelemetry()`
- **Conversation threading** for multi-turn conversations
- **Error handling** with telemetry correlation
@@ -182,7 +182,7 @@ Complete demo startup script that handles everything automatically.
```
**Features:**
- **Automatic configuration detection** - Checks for Azure OpenAI configuration
- **Automatic configuration detection** - Checks for Foundry configuration
- **Project building** - Automatically builds projects before running
- **Error handling** - Provides clear error messages if something goes wrong
- **Multi-window support** - Opens dashboard in separate window for better experience
@@ -201,10 +201,10 @@ If you encounter port binding errors, try:
2. Or kill any processes using the conflicting ports
### Authentication Issues
- Ensure your Azure OpenAI endpoint is correctly configured
- Ensure your Foundry project endpoint is correctly configured
- Check that the environment variables are set in the correct terminal session
- Verify you're logged in with Azure CLI (`az login`) and have access to the Azure OpenAI resource
- Ensure the Azure OpenAI deployment name matches your actual deployment
- Verify you're logged in with Azure CLI (`az login`) and have access to the Foundry project
- Ensure the `FOUNDRY_MODEL` value matches an enabled model in your Foundry project
### Build Issues
- Ensure you're using .NET 10.0 SDK
@@ -216,7 +216,7 @@ If you encounter port binding errors, try:
```
AgentOpenTelemetry/
├── AgentOpenTelemetry.csproj # Project file with dependencies
├── Program.cs # Main application with Azure OpenAI agent integration
├── Program.cs # Main application with Foundry AIProjectClient agent integration
├── start-demo.ps1 # PowerShell script to start the demo
└── README.md # This file
```
@@ -14,6 +14,7 @@ var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT
// 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.
// You must dissable client side conversation storage for clients that support it
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
@@ -33,7 +33,7 @@ List<AITool> agentTools = [.. mcpTools.Cast<AITool>()];
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
AIAgent agent = aiProjectClient.AsAIAgent(deploymentName,
instructions: "You are a helpful assistant that can help with Microsoft documentation questions. Use the Microsoft Learn MCP tool to search for documentation.",
instructions: "You are a helpful assistant that can help with Microsoft documentation questions. Use the Microsoft Learn MCP tool to search for documentation. In the output, indicate which tool you used if any.",
name: "DocsAgent",
tools: agentTools);
@@ -8,6 +8,7 @@ using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Assistants;
using OpenAI.Responses;
const string AgentInstructions = "You are a personal math tutor. When asked a math question, write and run code using the python tool to answer the question.";
const string AgentName = "CoderAgent-RAPI";
@@ -19,11 +20,41 @@ string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// The easiest way to add the hosted code interpreter is as follows:
/*
AIAgent agent = aiProjectClient.AsAIAgent(
deploymentName,
instructions: AgentInstructions,
name: AgentName,
tools: [new HostedCodeInterpreterTool() { Inputs = [] }]);
*/
// However, by default the reponses API does not return the output items from the hosted code interpreter tool.
// This is generally fine but for this sample we want to explicitly request those in the response generation configuration.
AIAgent agent = aiProjectClient
.GetProjectOpenAIClient()
.GetProjectResponsesClient()
.AsIChatClient(deploymentName)
.AsBuilder()
.ConfigureOptions(x =>
{
var previousFactory = x.RawRepresentationFactory;
x.RawRepresentationFactory = state =>
{
var responseOptions = previousFactory?.Invoke(state) as CreateResponseOptions ?? new CreateResponseOptions();
// Ensure that the response includes tool output items from the hosted code interpreter
responseOptions.IncludedProperties.Add(IncludedResponseProperty.CodeInterpreterCallOutputs);
return responseOptions;
};
})
.Build()
.AsAIAgent(
instructions: AgentInstructions,
name: AgentName,
tools: [new HostedCodeInterpreterTool() { Inputs = [] }]);
AgentResponse response = await agent.RunAsync("I need to solve the equation sin(x) + x^2 = 42");
@@ -56,14 +56,13 @@ try
// Inspect memory search results if available in raw response items.
foreach (var message in response.Messages)
{
if (message.RawRepresentation is MemorySearchToolCallResponseItem memorySearchResult)
if (message.RawRepresentation is MemorySearchToolCall memorySearchResult)
{
Console.WriteLine($"Memory Search Status: {memorySearchResult.Status}");
Console.WriteLine($"Memory Search Results Count: {memorySearchResult.Results.Count}");
Console.WriteLine($"Memory Search Results Count: {memorySearchResult.Memories.Count}");
foreach (var result in memorySearchResult.Results)
foreach (var memoryItem in memorySearchResult.Memories)
{
var memoryItem = result.MemoryItem;
Console.WriteLine($" - Memory ID: {memoryItem.MemoryId}");
Console.WriteLine($" Scope: {memoryItem.Scope}");
Console.WriteLine($" Content: {memoryItem.Content}");
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,17 +9,13 @@
<NoWarn>$(NoWarn);MAAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\SubprocessScriptRunner.cs" Link="SubprocessScriptRunner.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<PackageReference Include="Azure.Identity" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
<!-- Copy skills directory to output -->
@@ -9,14 +9,13 @@
//
// This sample uses a unit-converter skill that converts between miles, kilometers, pounds, and kilograms.
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
// --- Configuration ---
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// --- Skills Provider ---
// Discovers skills from the 'skills' directory containing SKILL.md files.
@@ -29,18 +28,17 @@ var skillsProvider = new AgentSkillsProvider(
// 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())
.GetResponsesClient()
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(new ChatClientAgentOptions
{
Name = "UnitConverterAgent",
ChatOptions = new()
{
ModelId = deploymentName,
Instructions = "You are a helpful assistant that can convert units.",
},
AIContextProviders = [skillsProvider],
},
model: deploymentName);
});
// --- Example: Unit conversion ---
Console.WriteLine("Converting units with file-based skills");
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -10,12 +10,8 @@
</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" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -9,14 +9,13 @@
// 3. Code scripts — executable delegates the agent can invoke directly
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
// --- Configuration ---
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// --- Build the code-defined skill ---
var unitConverterSkill = new AgentInlineSkill(
@@ -70,18 +69,17 @@ var skillsProvider = new AgentSkillsProvider(unitConverterSkill);
// 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())
.GetResponsesClient()
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(new ChatClientAgentOptions
{
Name = "UnitConverterAgent",
ChatOptions = new()
{
ModelId = deploymentName,
Instructions = "You are a helpful assistant that can convert units.",
},
AIContextProviders = [skillsProvider],
},
model: deploymentName);
});
// --- Example: Unit conversion ---
Console.WriteLine("Converting units with code-defined skills");
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -10,12 +10,8 @@
</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" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -5,14 +5,13 @@
using System.ComponentModel;
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
// --- Configuration ---
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// --- Class-Based Skill ---
// Instantiate the skill class.
@@ -25,18 +24,17 @@ var skillsProvider = new AgentSkillsProvider(unitConverter);
// 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())
.GetResponsesClient()
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(new ChatClientAgentOptions
{
Name = "UnitConverterAgent",
ChatOptions = new()
{
ModelId = deploymentName,
Instructions = "You are a helpful assistant that can convert units.",
},
AIContextProviders = [skillsProvider],
},
model: deploymentName);
});
// --- Example: Unit conversion ---
Console.WriteLine("Converting units with class-based skills");
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,17 +9,13 @@
<NoWarn>$(NoWarn);MAAI001;IDE0051</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\SubprocessScriptRunner.cs" Link="SubprocessScriptRunner.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<PackageReference Include="Azure.Identity" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
<!-- Copy skills directory to output -->
@@ -15,15 +15,14 @@
using System.ComponentModel;
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Responses;
// --- Configuration ---
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// --- 1. Code-Defined Skill: volume-converter ---
var volumeConverterSkill = new AgentInlineSkill(
@@ -67,18 +66,17 @@ var skillsProvider = new AgentSkillsProviderBuilder()
// 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())
.GetResponsesClient()
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(new ChatClientAgentOptions
{
Name = "MultiConverterAgent",
ChatOptions = new()
{
ModelId = deploymentName,
Instructions = "You are a helpful assistant that can convert units, volumes, and temperatures.",
},
AIContextProviders = [skillsProvider],
},
model: deploymentName);
});
// --- Example: Use all three skills ---
Console.WriteLine("Converting with mixed skills (file + code + class)");
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -10,13 +10,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -15,15 +15,14 @@
using System.ComponentModel;
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.DependencyInjection;
using OpenAI.Responses;
// --- Configuration ---
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// --- DI Container ---
// Register application services that skill resources and scripts can resolve at execution time.
@@ -83,19 +82,18 @@ var skillsProvider = new AgentSkillsProvider(distanceSkill, weightSkill);
// 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())
.GetResponsesClient()
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(
options: new ChatClientAgentOptions
{
Name = "UnitConverterAgent",
ChatOptions = new()
{
ModelId = deploymentName,
Instructions = "You are a helpful assistant that can convert units.",
},
AIContextProviders = [skillsProvider],
},
model: deploymentName,
services: serviceProvider);
// --- Example: Unit conversion ---
@@ -10,7 +10,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="ModelContextProtocol" />
@@ -18,7 +17,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Mcp\Microsoft.Agents.AI.Mcp.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -12,7 +12,7 @@
// to discover and inject the skill into a ChatClientAgent.
using System.ComponentModel;
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.DependencyInjection;
@@ -20,7 +20,6 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Client;
using ModelContextProtocol.Server;
using OpenAI.Responses;
if (args.Length > 0 && args[0] == "--server")
{
@@ -29,9 +28,9 @@ if (args.Length > 0 && args[0] == "--server")
}
// --- Configuration ---
string openAiEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
string openAiEndpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// --- MCP client + skill discovery ---
// Launch this same assembly as a stdio MCP server in a child process.
@@ -54,18 +53,17 @@ var skillsProvider = new AgentSkillsProviderBuilder()
// 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(openAiEndpoint), new DefaultAzureCredential())
.GetResponsesClient()
AIAgent agent = new AIProjectClient(new Uri(openAiEndpoint), new DefaultAzureCredential())
.AsAIAgent(new ChatClientAgentOptions
{
Name = "SkillsAgent",
ChatOptions = new()
{
ModelId = deploymentName,
Instructions = "You are a helpful assistant. Use available skills to answer the user.",
},
AIContextProviders = [skillsProvider],
},
model: deploymentName);
});
// --- Run ---
Console.WriteLine(new string('-', 60));
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);MAAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\SubprocessScriptRunner.cs" Link="SubprocessScriptRunner.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
<!-- Copy skills directory to output -->
<ItemGroup>
<None Include="skills\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,90 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to configure auto-approval rules for skill tools using the
// UseToolApproval middleware. It builds on the file-based skills pattern from Step01, adding
// ToolApprovalAgent middleware with auto-approval rules so that read-only skill operations
// (load_skill, read_skill_resource) are approved automatically while script execution
// (run_skill_script) still requires explicit user approval.
//
// All tools exposed by AgentSkillsProvider always require approval by default.
// Auto-approval rules let you selectively bypass the approval prompt for safe operations.
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Responses;
// --- Configuration ---
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
// --- Skills Provider ---
// Discovers skills from the 'skills' directory containing SKILL.md files.
// The script runner runs file-based scripts (e.g. Python) as local subprocesses.
var skillsProvider = new AgentSkillsProvider(
Path.Combine(AppContext.BaseDirectory, "skills"),
SubprocessScriptRunner.RunAsync);
// --- Agent Setup ---
// 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())
.GetResponsesClient()
.AsAIAgent(new ChatClientAgentOptions
{
Name = "UnitConverterAgent",
ChatOptions = new()
{
Instructions = "You are a helpful assistant that can convert units.",
},
AIContextProviders = [skillsProvider],
},
model: deploymentName)
.AsBuilder()
.UseToolApproval(new ToolApprovalAgentOptions
{
// Auto-approve read-only skill tools (load_skill, read_skill_resource).
// run_skill_script will still require explicit user approval.
AutoApprovalRules = [AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule],
})
.Build();
// For other auto-approval options (all tools, custom lambdas, combining providers),
// see the README.md in this sample directory.
// --- Example: Unit conversion with auto-approval ---
Console.WriteLine("Converting units with file-based skills and auto-approval");
Console.WriteLine(new string('-', 60));
AgentSession session = await agent.CreateSessionAsync();
AgentResponse response = await agent.RunAsync(
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?",
session);
// Handle any pending approval requests (only script execution should require approval)
List<ToolApprovalRequestContent> approvalRequests = response.Messages
.SelectMany(m => m.Contents)
.OfType<ToolApprovalRequestContent>()
.ToList();
while (approvalRequests.Count > 0)
{
List<ChatMessage> userInputResponses = approvalRequests
.ConvertAll(functionApprovalRequest =>
{
var toolCall = (FunctionCallContent)functionApprovalRequest.ToolCall;
Console.WriteLine($"Approval required for: {toolCall.Name}. Reply Y to approve:");
bool approved = Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false;
return new ChatMessage(ChatRole.User, [functionApprovalRequest.CreateResponse(approved)]);
});
response = await agent.RunAsync(userInputResponses, session);
approvalRequests = response.Messages
.SelectMany(m => m.Contents)
.OfType<ToolApprovalRequestContent>()
.ToList();
}
Console.WriteLine($"Agent: {response.Text}");
@@ -0,0 +1,102 @@
# Skills Auto-Approval Sample
This sample demonstrates how to configure **auto-approval rules** for skill tools using the `UseToolApproval` middleware and `AgentSkillsProvider`'s built-in approval rules.
It builds on the [file-based skills sample](../Agent_Step01_FileBasedSkills/) by adding `ToolApprovalAgent` middleware that auto-approves read-only skill operations while still prompting for script execution.
## What it demonstrates
- All tools exposed by `AgentSkillsProvider` (`load_skill`, `read_skill_resource`, `run_skill_script`) always require approval by default
- Multiple ways to configure auto-approval (see below)
- Handling approval prompts for script execution via `ToolApprovalRequestContent`
## Configuring Auto-Approval
Auto-approval rules are passed to `ToolApprovalAgentOptions.AutoApprovalRules` when calling `UseToolApproval`. Rules are evaluated in order; the first rule returning `true` auto-approves the call.
### Option 1: Built-in read-only rule
Auto-approves `load_skill` and `read_skill_resource` while still prompting for `run_skill_script`:
```csharp
.UseToolApproval(new ToolApprovalAgentOptions
{
AutoApprovalRules = [AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule],
})
```
### Option 2: Built-in all-tools rule
Auto-approves all three skill tools without prompting:
```csharp
.UseToolApproval(new ToolApprovalAgentOptions
{
AutoApprovalRules = [AgentSkillsProvider.AllToolsAutoApprovalRule],
})
```
### Option 3: Custom lambda rule
Provide your own logic as a `Func<FunctionCallContent, ValueTask<bool>>`. For example, to auto-approve only `load_skill`:
```csharp
.UseToolApproval(new ToolApprovalAgentOptions
{
AutoApprovalRules =
[
(FunctionCallContent functionCall) =>
new ValueTask<bool>(functionCall.Name == AgentSkillsProvider.LoadSkillToolName),
],
})
```
### Combining rules from multiple providers
When using multiple providers (e.g., skills + file access), combine their rules in a single list:
```csharp
.UseToolApproval(new ToolApprovalAgentOptions
{
AutoApprovalRules =
[
AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule,
FileAccessProvider.ReadOnlyToolsAutoApprovalRule,
],
})
```
## Skills Included
### unit-converter
Converts between common units (miles↔km, pounds↔kg) using a multiplication factor.
- `references/conversion-table.md` — Conversion factor table
- `scripts/convert.py` — Python script that performs the conversion
## Running the Sample
### Prerequisites
- .NET 10.0 SDK
- Azure OpenAI endpoint with a deployed model
- Python 3 installed and available as `python3` on your PATH
### Setup
```bash
export AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com/"
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini"
```
### Run
```bash
dotnet run
```
### Expected Behavior
- `load_skill` and `read_skill_resource` calls are auto-approved (no user prompt)
- `run_skill_script` calls prompt the user for approval before executing
@@ -0,0 +1,11 @@
---
name: unit-converter
description: Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.
---
## Usage
When the user requests a unit conversion:
1. First, review `references/conversion-table.md` to find the correct factor
2. Run the `scripts/convert.py` script with `--value <number> --factor <factor>` (e.g. `--value 26.2 --factor 1.60934`)
3. Present the converted value clearly with both units
@@ -0,0 +1,10 @@
# Conversion Tables
Formula: **result = value × factor**
| From | To | Factor |
|-------------|-------------|----------|
| miles | kilometers | 1.60934 |
| kilometers | miles | 0.621371 |
| pounds | kilograms | 0.453592 |
| kilograms | pounds | 2.20462 |
@@ -0,0 +1,29 @@
# Unit conversion script
# Converts a value using a multiplication factor: result = value × factor
#
# Usage:
# python scripts/convert.py --value 26.2 --factor 1.60934
# python scripts/convert.py --value 75 --factor 2.20462
import argparse
import json
def main() -> None:
parser = argparse.ArgumentParser(
description="Convert a value using a multiplication factor.",
epilog="Examples:\n"
" python scripts/convert.py --value 26.2 --factor 1.60934\n"
" python scripts/convert.py --value 75 --factor 2.20462",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--value", type=float, required=True, help="The numeric value to convert.")
parser.add_argument("--factor", type=float, required=True, help="The conversion factor from the table.")
args = parser.parse_args()
result = round(args.value * args.factor, 4)
print(json.dumps({"value": args.value, "factor": args.factor, "result": result}))
if __name__ == "__main__":
main()
@@ -9,13 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Hyperlight\Microsoft.Agents.AI.Hyperlight.csproj" />
</ItemGroup>
@@ -4,14 +4,13 @@
// code interpreter: the model can write and execute arbitrary Python code to
// answer quantitative questions without calling any additional tools.
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hyperlight;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
var guestPath = Environment.GetEnvironmentVariable("HYPERLIGHT_PYTHON_GUEST_PATH") ?? throw new InvalidOperationException("HYPERLIGHT_PYTHON_GUEST_PATH is not set.");
using var codeAct = new HyperlightCodeActProvider(HyperlightCodeActProviderOptions.CreateForWasm(guestPath));
@@ -19,13 +18,12 @@ using var codeAct = new HyperlightCodeActProvider(HyperlightCodeActProviderOptio
// 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(
AIAgent agent = new AIProjectClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName)
.AsAIAgent(new ChatClientAgentOptions()
{
ChatOptions = new() { Instructions = "You are a helpful assistant. When the user asks something quantitative, write Python and call `execute_code` instead of guessing." },
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful assistant. When the user asks something quantitative, write Python and call `execute_code` instead of guessing." },
AIContextProviders = [codeAct],
});
@@ -9,13 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Hyperlight\Microsoft.Agents.AI.Hyperlight.csproj" />
</ItemGroup>
@@ -7,15 +7,14 @@
// ApprovalRequiredAIFunction so any code that reaches it requires user approval
// for the entire execute_code invocation.
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hyperlight;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
var guestPath = Environment.GetEnvironmentVariable("HYPERLIGHT_PYTHON_GUEST_PATH") ?? throw new InvalidOperationException("HYPERLIGHT_PYTHON_GUEST_PATH is not set.");
AIFunction fetchDocs = AIFunctionFactory.Create(
@@ -42,13 +41,12 @@ using var codeAct = new HyperlightCodeActProvider(options);
// 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(
AIAgent agent = new AIProjectClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName)
.AsAIAgent(new ChatClientAgentOptions()
{
ChatOptions = new() { Instructions = "You are a helpful assistant. Prefer orchestrating your work in a single `execute_code` block using `call_tool(...)` over issuing many direct tool calls." },
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful assistant. Prefer orchestrating your work in a single `execute_code` block using `call_tool(...)` over issuing many direct tool calls." },
AIContextProviders = [codeAct],
});
@@ -9,13 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Hyperlight\Microsoft.Agents.AI.Hyperlight.csproj" />
</ItemGroup>
@@ -5,15 +5,14 @@
// when you want a fixed tool surface for the agent's lifetime and don't need
// the per-run snapshot/registry semantics of HyperlightCodeActProvider.
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hyperlight;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
var guestPath = Environment.GetEnvironmentVariable("HYPERLIGHT_PYTHON_GUEST_PATH") ?? throw new InvalidOperationException("HYPERLIGHT_PYTHON_GUEST_PATH is not set.");
AIFunction calculate = AIFunctionFactory.Create(
@@ -34,10 +33,9 @@ var instructions =
// 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(
AIAgent agent = new AIProjectClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName)
.AsAIAgent(instructions: instructions, tools: [executeCode]);
.AsAIAgent(model: deploymentName, instructions: instructions, tools: [executeCode]);
Console.WriteLine(await agent.RunAsync("What is 12.3 * 4.5? Use the multiply tool from within `execute_code`."));
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,14 +9,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.InMemory" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -3,17 +3,18 @@
// This sample shows how to create and use a simple AI agent that stores chat messages in a vector store using the ChatHistoryMemoryProvider.
// It can then use the chat history from prior conversations to inform responses in new conversations.
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.InMemory;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
var embeddingDeploymentName = Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_MODEL") ?? "text-embedding-3-large";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Create a vector store to store the chat messages in.
// For demonstration purposes, we are using an in-memory vector store.
@@ -23,19 +24,17 @@ VectorStore vectorStore = new InMemoryVectorStore(new InMemoryVectorStoreOptions
// 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.
EmbeddingGenerator = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
EmbeddingGenerator = aiProjectClient
.GetProjectOpenAIClient()
.GetEmbeddingClient(embeddingDeploymentName)
.AsIEmbeddingGenerator()
});
// Create the agent and add the ChatHistoryMemoryProvider to store chat messages in the vector store.
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName)
AIAgent agent = aiProjectClient
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are good at telling jokes." },
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are good at telling jokes." },
Name = "Joker",
AIContextProviders = [new ChatHistoryMemoryProvider(
vectorStore,
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,13 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Mem0\Microsoft.Agents.AI.Mem0.csproj" />
</ItemGroup>
@@ -6,15 +6,13 @@
using System.Net.Http.Headers;
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Mem0;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
var mem0ServiceUri = Environment.GetEnvironmentVariable("MEM0_ENDPOINT") ?? throw new InvalidOperationException("MEM0_ENDPOINT is not set.");
var mem0ApiKey = Environment.GetEnvironmentVariable("MEM0_API_KEY") ?? throw new InvalidOperationException("MEM0_API_KEY is not set.");
@@ -24,16 +22,15 @@ using HttpClient mem0HttpClient = new();
mem0HttpClient.BaseAddress = new Uri(mem0ServiceUri);
mem0HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", mem0ApiKey);
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// 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)
AIAgent agent = aiProjectClient
.AsAIAgent(new ChatClientAgentOptions()
{
ChatOptions = new() { Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
// The stateInitializer can be used to customize the Mem0 scope per session and it will be called each time a session
// is encountered by the Mem0Provider that does not already have Mem0Provider state stored on the session.
// If each session should have its own Mem0 scope, you can create a new id per session via the stateInitializer, e.g.:
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,13 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Valkey\Microsoft.Agents.AI.Valkey.csproj" />
</ItemGroup>
@@ -8,16 +8,15 @@
// docker run -d --name valkey -p 6379:6379 valkey/valkey:latest
// - Azure OpenAI endpoint and deployment configured via environment variables
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Valkey;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
using Valkey.Glide;
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";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
var valkeyConnection = Environment.GetEnvironmentVariable("VALKEY_CONNECTION") ?? "localhost:6379";
var connection = await ConnectionMultiplexer.ConnectAsync(valkeyConnection);
@@ -33,11 +32,10 @@ var historyProvider = new ValkeyChatHistoryProvider(
MaxMessages = 20
});
AIAgent historyAgent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetChatClient(deploymentName)
AIAgent historyAgent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(new ChatClientAgentOptions()
{
ChatOptions = new() { Instructions = "You are a helpful assistant that remembers our conversation." },
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful assistant that remembers our conversation." },
ChatHistoryProvider = historyProvider
});
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,14 +9,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.InMemory" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -5,30 +5,30 @@
// When the agent is invoked, it searches the vector store for relevant older messages and
// prepends them as a "memory" context message before the recent session history.
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.InMemory;
using OpenAI.Chat;
using SampleApp;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
var embeddingDeploymentName = Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_MODEL") ?? "text-embedding-3-large";
// 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 credential = new DefaultAzureCredential();
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Create a vector store to store overflow chat messages.
// For demonstration purposes, we are using an in-memory vector store.
// Replace this with a persistent vector store implementation for production scenarios.
VectorStore vectorStore = new InMemoryVectorStore(new InMemoryVectorStoreOptions()
{
EmbeddingGenerator = new AzureOpenAIClient(new Uri(endpoint), credential)
EmbeddingGenerator = aiProjectClient
.GetProjectOpenAIClient()
.GetEmbeddingClient(embeddingDeploymentName)
.AsIEmbeddingGenerator()
});
@@ -49,11 +49,10 @@ var boundedProvider = new BoundedChatHistoryProvider(
searchScope: new() { UserId = "UID1" }));
// Create the agent with the bounded chat history provider.
AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), credential)
.GetChatClient(deploymentName)
AIAgent agent = aiProjectClient
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are a helpful assistant. Answer questions concisely." },
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful assistant. Answer questions concisely." },
Name = "Assistant",
ChatHistoryProvider = boundedProvider,
});
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,14 +9,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.InMemory" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -5,30 +5,29 @@
// The TextSearchProvider runs a search against the vector store via the TextSearchStore before each model invocation and injects the results into the model context.
// The TextSearchStore is a sample store implementation that hardcodes a storage schema and uses the vector store to store and retrieve documents.
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Samples;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.InMemory;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
var embeddingDeploymentName = Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_MODEL") ?? "text-embedding-3-large";
// 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.
AzureOpenAIClient azureOpenAIClient = new(
AIProjectClient aiProjectClient = new(
new Uri(endpoint),
new DefaultAzureCredential());
// Create an In-Memory vector store that uses the Azure OpenAI embedding model to generate embeddings.
// Create an In-Memory vector store that uses the Azure AI Foundry embedding model to generate embeddings.
VectorStore vectorStore = new InMemoryVectorStore(new()
{
EmbeddingGenerator = azureOpenAIClient.GetEmbeddingClient(embeddingDeploymentName).AsIEmbeddingGenerator()
EmbeddingGenerator = aiProjectClient.GetProjectOpenAIClient().GetEmbeddingClient(embeddingDeploymentName).AsIEmbeddingGenerator()
});
// Create a store that defines a storage schema, and uses the vector store to store and retrieve documents.
@@ -60,11 +59,10 @@ TextSearchProviderOptions textSearchOptions = new()
};
// Create the AI agent with the TextSearchProvider as the AI context provider.
AIAgent agent = azureOpenAIClient
.GetChatClient(deploymentName)
AIAgent agent = aiProjectClient
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
ChatOptions = new() { 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(SearchAdapter, textSearchOptions)],
// Since we are using ChatCompletion which stores chat history locally, we can also add a message filter
// that removes messages produced by the TextSearchProvider before they are added to the chat history, so that
@@ -9,14 +9,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Qdrant" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -4,33 +4,32 @@
// While the sample is using Qdrant, it can easily be replaced with any other vector store that implements the Microsoft.Extensions.VectorData abstractions.
// The TextSearchProvider runs a search against the vector store before each model invocation and injects the results into the model context.
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.Qdrant;
using OpenAI.Chat;
using Qdrant.Client;
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";
var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
var embeddingDeploymentName = Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_MODEL") ?? "text-embedding-3-large";
var afOverviewUrl = "https://raw.githubusercontent.com/MicrosoftDocs/semantic-kernel-docs/refs/heads/main/agent-framework/overview/index.md";
var afMigrationUrl = "https://raw.githubusercontent.com/MicrosoftDocs/semantic-kernel-docs/refs/heads/main/agent-framework/migration-guide/from-semantic-kernel/index.md";
// 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.
AzureOpenAIClient azureOpenAIClient = new(
AIProjectClient aiProjectClient = new(
new Uri(endpoint),
new DefaultAzureCredential());
// Create a Qdrant vector store that uses the Azure OpenAI embedding model to generate embeddings.
// Create a Qdrant vector store that uses the Azure AI Foundry embedding model to generate embeddings.
QdrantClient client = new("localhost");
VectorStore vectorStore = new QdrantVectorStore(client, ownsClient: true, new()
{
EmbeddingGenerator = azureOpenAIClient.GetEmbeddingClient(embeddingDeploymentName).AsIEmbeddingGenerator()
EmbeddingGenerator = aiProjectClient.GetProjectOpenAIClient().GetEmbeddingClient(embeddingDeploymentName).AsIEmbeddingGenerator()
});
// Create a collection and upsert some text into it.
@@ -69,11 +68,10 @@ TextSearchProviderOptions textSearchOptions = new()
};
// Create the AI agent with the TextSearchProvider as the AI context provider.
AIAgent agent = azureOpenAIClient
.GetChatClient(deploymentName)
AIAgent agent = aiProjectClient
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief." },
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief." },
AIContextProviders = [new TextSearchProvider(SearchAdapter, textSearchOptions)],
// Configure a filter on the InMemoryChatHistoryProvider so that we don't persist the messages produced by the TextSearchProvider in chat history.
// The default is to persist all messages except those that came from chat history in the first place.
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,13 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -6,14 +6,13 @@
// The provider invokes the custom search function
// before each model invocation and injects the results into the model context.
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
TextSearchProviderOptions textSearchOptions = new()
{
@@ -25,13 +24,12 @@ TextSearchProviderOptions textSearchOptions = new()
// 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(
AIAgent agent = new AIProjectClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName)
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
ChatOptions = new() { 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)]
});
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -20,15 +20,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
<PackageReference Include="Azure.Identity" Version="1.19.0" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-rc4" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.6.0" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.2.0" />
<PackageReference Include="Neo4j.AgentFramework.GraphRAG" Version="0.1.0-preview.2" />
<PackageReference Include="Neo4j.Driver" Version="5.28.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -1,14 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Neo4j.AgentFramework.GraphRAG;
using Neo4j.Driver;
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";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
var neo4jUri = Environment.GetEnvironmentVariable("NEO4J_URI") ?? throw new InvalidOperationException("NEO4J_URI is not set.");
var neo4jUsername = Environment.GetEnvironmentVariable("NEO4J_USERNAME") ?? "neo4j";
var neo4jPassword = Environment.GetEnvironmentVariable("NEO4J_PASSWORD") ?? throw new InvalidOperationException("NEO4J_PASSWORD is not set.");
@@ -48,15 +48,14 @@ await using var provider = new Neo4jContextProvider(
// 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(
AIAgent agent = new AIProjectClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName)
.AsIChatClient()
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new()
{
ModelId = deploymentName,
Instructions = "You are a helpful assistant that answers questions using Neo4j graph context."
},
AIContextProviders = [provider]
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,13 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -1,20 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use a ChatClientAgent with function tools that require a human in the loop for approvals.
// It shows both non-streaming and streaming agent interactions using menu-related tools.
// If the agent is hosted in a service, with a remote user, combine this sample with the Persisted Conversations sample to persist the chat history
// while the agent is waiting for user input.
// Function Tools with Approvals — Human-in-the-loop tool execution
//
// This sample demonstrates how to use function tools that require human
// approval before execution. It shows both non-streaming and streaming
// agent interactions using menu-related tools.
// If the agent is hosted in a service, combine this with the Persisted
// Conversations sample to persist chat history while waiting for user input.
using System.ComponentModel;
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
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";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// Create a sample function tool that the agent can use.
[Description("Get the weather for a given location.")]
@@ -26,11 +28,10 @@ static string GetWeather([Description("The location to get the weather for.")] s
// 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(
AIAgent agent = new AIProjectClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName)
.AsAIAgent(instructions: "You are a helpful assistant", tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))]);
.AsAIAgent(model: deploymentName, instructions: "You are a helpful assistant", tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))]);
// Call the agent and check if there are any function approval requests to handle.
// For simplicity, we are assuming here that only function approvals are pending.
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,13 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -1,29 +1,29 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to configure ChatClientAgent to produce structured output.
// Structured Output — Configure agents to return typed JSON
//
// This sample shows how to configure a ChatClientAgent to produce
// structured output using JSON schema constraints with Azure AI Foundry.
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.AI.OpenAI;
using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
using SampleApp;
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// Create chat client to be used by chat client agents.
// Create AI Project client to be used by chat client agents.
// 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.
ChatClient chatClient = new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName);
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
// Demonstrates how to work with structured output via ResponseFormat with the non-generic RunAsync method.
// This approach is useful when:
@@ -31,35 +31,36 @@ ChatClient chatClient = new AzureOpenAIClient(
// and passes it as text to another agent as input, without the need for the caller to directly work with the structured output.
// b. The type of the structured output is not known at compile time, so the generic RunAsync<T> method cannot be used.
// c. The type of the structured output is represented by JSON schema only, without a corresponding class or type in the code.
await UseStructuredOutputWithResponseFormatAsync(chatClient);
await UseStructuredOutputWithResponseFormatAsync(aiProjectClient, deploymentName);
// Demonstrates how to work with structured output via the generic RunAsync<T> method.
// This approach is useful when the caller needs to directly work with the structured output in the code
// via an instance of the corresponding class or type and the type is known at compile time.
await UseStructuredOutputWithRunAsync(chatClient);
await UseStructuredOutputWithRunAsync(aiProjectClient, deploymentName);
// Demonstrates how to work with structured output when streaming using the RunStreamingAsync method.
await UseStructuredOutputWithRunStreamingAsync(chatClient);
await UseStructuredOutputWithRunStreamingAsync(aiProjectClient, deploymentName);
// Demonstrates how to add structured output support to agents that don't natively support it using the structured output middleware.
// This approach is useful when working with agents that don't support structured output natively, or agents using models
// that don't have the capability to produce structured output, allowing you to still leverage structured output features by transforming
// the text output from the agent into structured data using a chat client.
await UseStructuredOutputWithMiddlewareAsync(chatClient);
await UseStructuredOutputWithMiddlewareAsync(aiProjectClient, deploymentName);
static async Task UseStructuredOutputWithResponseFormatAsync(ChatClient chatClient)
static async Task UseStructuredOutputWithResponseFormatAsync(AIProjectClient aiProjectClient, string deploymentName)
{
Console.WriteLine("=== Structured Output with ResponseFormat ===");
// Create the agent
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions()
AIAgent agent = aiProjectClient.AsAIAgent(new ChatClientAgentOptions()
{
Name = "HelpfulAssistant",
ChatOptions = new()
{
ModelId = deploymentName,
Instructions = "You are a helpful assistant.",
// Specify CityInfo as the type parameter of ForJsonSchema to indicate the expected structured output from the agent.
ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<CityInfo>()
ResponseFormat = ChatResponseFormat.ForJsonSchema<CityInfo>()
}
});
@@ -81,12 +82,12 @@ static async Task UseStructuredOutputWithResponseFormatAsync(ChatClient chatClie
Console.WriteLine();
}
static async Task UseStructuredOutputWithRunAsync(ChatClient chatClient)
static async Task UseStructuredOutputWithRunAsync(AIProjectClient aiProjectClient, string deploymentName)
{
Console.WriteLine("=== Structured Output with RunAsync<T> ===");
// Create the agent
AIAgent agent = chatClient.AsAIAgent(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
AIAgent agent = aiProjectClient.AsAIAgent(deploymentName, name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
// Set CityInfo as the type parameter of RunAsync method to specify the expected structured output from the agent and invoke it with some unstructured input.
AgentResponse<CityInfo> response = await agent.RunAsync<CityInfo>("Provide information about the capital of France.");
@@ -99,19 +100,20 @@ static async Task UseStructuredOutputWithRunAsync(ChatClient chatClient)
Console.WriteLine();
}
static async Task UseStructuredOutputWithRunStreamingAsync(ChatClient chatClient)
static async Task UseStructuredOutputWithRunStreamingAsync(AIProjectClient aiProjectClient, string deploymentName)
{
Console.WriteLine("=== Structured Output with RunStreamingAsync ===");
// Create the agent
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions()
AIAgent agent = aiProjectClient.AsAIAgent(new ChatClientAgentOptions()
{
Name = "HelpfulAssistant",
ChatOptions = new()
{
ModelId = deploymentName,
Instructions = "You are a helpful assistant.",
// Specify CityInfo as the type parameter of ForJsonSchema to indicate the expected structured output from the agent.
ResponseFormat = Microsoft.Extensions.AI.ChatResponseFormat.ForJsonSchema<CityInfo>()
ResponseFormat = ChatResponseFormat.ForJsonSchema<CityInfo>()
}
});
@@ -129,12 +131,12 @@ static async Task UseStructuredOutputWithRunStreamingAsync(ChatClient chatClient
Console.WriteLine();
}
static async Task UseStructuredOutputWithMiddlewareAsync(ChatClient chatClient)
static async Task UseStructuredOutputWithMiddlewareAsync(AIProjectClient aiProjectClient, string deploymentName)
{
Console.WriteLine("=== Structured Output with UseStructuredOutput Middleware ===");
// Create chat client that will transform the agent text response into structured output.
IChatClient meaiChatClient = chatClient.AsIChatClient();
IChatClient meaiChatClient = aiProjectClient.GetProjectOpenAIClient().GetProjectResponsesClientForModel(deploymentName).AsIChatClientWithStoredOutputDisabled(deploymentName);
// Create the agent
AIAgent agent = meaiChatClient.AsAIAgent(name: "HelpfulAssistant", instructions: "You are a helpful assistant.");
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,13 +9,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -2,26 +2,27 @@
#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances
// This sample shows how to create and use a simple AI agent with a conversation that can be persisted to disk.
// Persisted Conversations — Save and restore chat history to disk
//
// This sample shows how to persist an agent conversation to disk
// so it can be resumed across process restarts.
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Chat;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// Create the agent
// 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(
AIAgent agent = new AIProjectClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName)
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker");
.AsAIAgent(model: deploymentName, instructions: "You are good at telling jokes.", name: "Joker");
// Start a new session for the agent conversation.
AgentSession session = await agent.CreateSessionAsync();
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,14 +9,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.InMemory" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -2,23 +2,25 @@
#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances
// This sample shows how to create and use a simple AI agent with custom ChatHistoryProvider that stores chat history in a custom storage location.
// The state of the custom ChatHistoryProvider (SessionDbKey) is stored in the AgentSession's StateBag, so that when the session is resumed later,
// the chat history can be retrieved from the custom storage location.
// Third-Party Chat History Storage — Custom ChatHistoryProvider
//
// This sample shows how to use a custom ChatHistoryProvider that stores
// chat history in an external location. The provider's state (SessionDbKey)
// is stored in AgentSession.StateBag so conversations can be resumed later.
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.InMemory;
using OpenAI.Chat;
using SampleApp;
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
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";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// Create a vector store to store the chat messages in.
// Replace this with a vector store implementation of your choice if you want to persist the chat history to disk.
@@ -28,16 +30,18 @@ VectorStore vectorStore = new InMemoryVectorStore();
// 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(
AIAgent agent = new AIProjectClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName)
.GetProjectOpenAIClient()
.GetProjectResponsesClient()
.AsIChatClientWithStoredOutputDisabled(deploymentName)
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "You are good at telling jokes." },
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are good at telling jokes." },
Name = "Joker",
// Create a new ChatHistoryProvider for this agent that stores chat history in a vector store.
ChatHistoryProvider = new VectorChatHistoryProvider(vectorStore)
ChatHistoryProvider = new VectorChatHistoryProvider(vectorStore),
});
// Start a new session for the agent conversation.
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,16 +9,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="OpenTelemetry.Exporter.Console" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -1,17 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to create and use a simple AI agent with Azure OpenAI as the backend that logs telemetry using OpenTelemetry.
// Agent Observability — OpenTelemetry tracing with Azure AI Foundry
//
// This sample shows how to instrument an AI agent with OpenTelemetry
// for distributed tracing and telemetry logging.
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Azure.Monitor.OpenTelemetry.Exporter;
using Microsoft.Agents.AI;
using OpenAI.Chat;
using OpenTelemetry;
using OpenTelemetry.Trace;
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";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
var applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING");
// Create TracerProvider with console exporter
@@ -30,9 +32,8 @@ using var tracerProvider = tracerProviderBuilder.Build();
// 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)
.AsAIAgent(instructions: "You are good at telling jokes.", name: "Joker")
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(model: deploymentName, instructions: "You are good at telling jokes.", name: "Joker")
.AsBuilder()
.UseOpenTelemetry(sourceName: sourceName)
.Build();
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,14 +9,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -2,38 +2,30 @@
#pragma warning disable CA1812
// This sample shows how to use dependency injection to register an AIAgent and use it from a hosted service with a user input chat loop.
// Dependency Injection — Register and resolve agents via DI
//
// This sample shows how to use dependency injection to register an
// AIAgent and consume it from a hosted service with a chat loop.
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
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";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// Create a host builder that we will register services with and then run.
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Add agent options to the service collection.
builder.Services.AddSingleton(new ChatClientAgentOptions() { Name = "Joker", ChatOptions = new() { Instructions = "You are good at telling jokes." } });
// Add a chat client to the service collection.
// Create the AI agent from the Azure AI Foundry project client.
// 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.
builder.Services.AddKeyedChatClient("AzureOpenAI", (sp) => new AzureOpenAIClient(
new Uri(endpoint),
new DefaultAzureCredential())
.GetChatClient(deploymentName)
.AsIChatClient());
// Add the AI agent to the service collection.
builder.Services.AddSingleton<AIAgent>((sp) => new ChatClientAgent(
chatClient: sp.GetRequiredKeyedService<IChatClient>("AzureOpenAI"),
options: sp.GetRequiredService<ChatClientAgentOptions>()));
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
AIAgent agent = aiProjectClient.AsAIAgent(model: deploymentName, name: "Joker", instructions: "You are good at telling jokes.");
builder.Services.AddSingleton(agent);
// Add a sample service that will use the agent to respond to user input.
builder.Services.AddHostedService<SampleService>();
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,12 +9,8 @@
</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" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
<ItemGroup>
@@ -1,24 +1,25 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to use Image Multi-Modality with an AI agent.
// Using Images — Multimodal input with an AI agent
//
// This sample shows how to send image content to an AI agent
// for vision-based analysis.
using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = System.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 agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetChatClient(deploymentName)
var agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(
name: "VisionAgent",
instructions: "You are a helpful agent that can analyze images");
model: deploymentName,
instructions: "You are a helpful agent that can analyze images",
name: "VisionAgent");
ChatMessage message = new(ChatRole.User, [
new TextContent("What do you see in this image?"),

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