Compare commits

...

83 Commits

Author SHA1 Message Date
Chris Gillum b316691c18 Add note about issue transfers 2026-07-21 15:02:44 -07:00
Chris Gillum 8fad6ce679 docs: generalize publish-lag example in ADR-0032
Replace the WorkflowHitlContext-specific illustration with a generic description of the publish-lag mechanism. The named symbol is currently exported by the extension and present in core's shim, so using it as an 'unpublished' example read as internally inconsistent.

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

Copilot-Session: 6181dcf9-857b-43ea-9fd2-fcd6b175ffdd
2026-07-21 14:48:52 -07:00
Chris Gillum bc076bf360 Fix GitHub user handles 2026-07-21 14:46:06 -07:00
Chris Gillum dc14e30503 docs: add ADR-0032 proposing durable/Azure Functions repo extraction
Proposes extracting the Durable Task and Azure Functions hosting integrations into a dedicated repository (microsoft/agent-framework-durable-extension), keeping a backward-compatible shim and the [all] extra so the move is invisible to consumers. Status: proposed, for stakeholder signoff ahead of the code-removal PR.

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

Copilot-Session: 6181dcf9-857b-43ea-9fd2-fcd6b175ffdd
2026-07-21 14:34:49 -07:00
Giles Odigwe b6b16ddb75 Python: forward GitHubCopilotOptions verbatim to create_session (#7155)
* Python: forward GitHubCopilotOptions verbatim to create_session

Refactor the GitHub Copilot agent to forward the full options dict to the
Copilot SDK's create_session/resume_session instead of hand-mapping a fixed
subset. GitHubCopilotOptions stays as the curated, typed surface, but any
other create_session parameter (reasoning_effort, context_tier,
enable_citations, ...) is now passed through verbatim. Unknown keys surface
as TypeError from the SDK instead of being silently dropped.

De-duplicates the near-identical _create_session/_resume_session bodies into
a shared _build_session_kwargs helper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f9d016d5-4d8c-43f0-a8fb-f3cf3d1ad7eb

* Python: address review feedback on GHCP options passthrough

- Strip agent-internal/client-level keys (on_pre_tool_use, on_function_approval,
  timeout, cli_path, log_level, base_directory) from the forwarded kwargs so they
  cannot leak into create_session/resume_session and raise TypeError.
- Source caller tools from the merged options layer so tools supplied via
  default_options are honored instead of silently dropped.
- Honor a caller-supplied native 'hooks' dict in _build_session_hooks (composing
  with the on_pre_tool_use shortcut) instead of unconditionally overwriting it.
- Validate mock create_session/resume_session calls against the real SDK
  signatures in tests so invalid kwargs surface as TypeError, and add regression
  tests for the passthrough contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f9d016d5-4d8c-43f0-a8fb-f3cf3d1ad7eb

* Python: avoid redundant re-read of model in _build_session_kwargs

model is popped from default_options into settings at init, so a per-run
model already lands in the merged kwargs. Keep that value when present and
only fall back to the resolved setting otherwise, instead of re-reading opts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f9d016d5-4d8c-43f0-a8fb-f3cf3d1ad7eb

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-20 18:08:55 +00:00
Evan Mattson c218067646 Python: Consolidate dependency updates (#7204)
* Bump uv from 0.11.28 to 0.11.29 in /python

Bumps [uv](https://github.com/astral-sh/uv) from 0.11.28 to 0.11.29.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.11.28...0.11.29)

---
updated-dependencies:
- dependency-name: uv
  dependency-version: 0.11.29
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* Bump zuban from 0.8.2 to 0.9.0 in /python

Bumps [zuban](https://github.com/zubanls/zubanls-python) from 0.8.2 to 0.9.0.
- [Release notes](https://github.com/zubanls/zubanls-python/releases)
- [Commits](https://github.com/zubanls/zubanls-python/compare/v0.8.2...v0.9.0)

---
updated-dependencies:
- dependency-name: zuban
  dependency-version: 0.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* PR #7146: Bump ty from 0.0.55 to 0.0.60 in /python

* Bump ruff from 0.15.20 to 0.15.22 in /python

Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.20 to 0.15.22.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.20...0.15.22)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.21
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* Bump mypy from 2.2.0 to 2.3.0 in /python

Bumps [mypy](https://github.com/python/mypy) from 2.2.0 to 2.3.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v2.2.0...v2.3.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 2.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* Bump prek from 0.4.8 to 0.4.10 in /python

Bumps [prek](https://github.com/j178/prek) from 0.4.8 to 0.4.10.
- [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.8...v0.4.10)

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

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

* Bump azure-ai-projects from 2.2.0 to 2.3.0 in /python

Bumps azure-ai-projects from 2.2.0 to 2.3.0.

---
updated-dependencies:
- dependency-name: azure-ai-projects
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* Bump types-python-dateutil in /python

Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.9.0.20260518 to 2.9.0.20260716.
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-python-dateutil
  dependency-version: 2.9.0.20260716
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* Bump mypy from 2.2.0 to 2.3.0 in /python

Bumps [mypy](https://github.com/python/mypy) from 2.2.0 to 2.3.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v2.2.0...v2.3.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 2.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* Bump botocore from 1.43.45 to 1.43.49 in /python

Bumps [botocore](https://github.com/boto/botocore) from 1.43.45 to 1.43.49.
- [Commits](https://github.com/boto/botocore/compare/1.43.45...1.43.49)

---
updated-dependencies:
- dependency-name: botocore
  dependency-version: 1.43.49
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

* PRs #7144/#7147: Align lab uv and ruff pins

* Regenerate lockfile for Python Dependabot PRs #7144-#7153

* Python: Support azure-ai-projects 2.3 session operations (#7150)

* Python: Apply Ruff 0.15.22 suppression updates (#7147)

* Python: Update tests for ty 0.0.60 (#7146)

* Python: Update Foundry samples for azure-ai-projects 2.3 (#7150)

* Python: Address dependency rollup review comments

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 18:01:24 +00:00
westey ac474100ce Python: [BREAKING] Graduate create_harness_agent out of experimental (#7120)
* Graduate harness agent

* Add agents.md update

* Fix build errors

* Address PR comments

* Fix build error
2026-07-20 18:00:28 +00:00
Theo van Kraay a057cd505c Python: Add agent-framework-azure-cosmos-memory context provider (#6719)
* Add agent-framework-azure-cosmos-memory context provider (draft)

Introduces CosmosMemoryContextProvider, a ContextProvider that wraps the azure-cosmos-agent-memory toolkit to give agents long-term, Cosmos DB-backed memory (fact/procedural recall + user summaries). Includes package scaffolding, unit tests (mocked client), live Azure integration tests (marked), samples, README, and AGENTS.md.

Draft: uv.lock is intentionally left unchanged. This package depends on azure-cosmos-agent-memory (requires Python >=3.11), which is unsatisfiable against the workspace's current >=3.10 floor, so adding it to the shared lock requires a workspace decision (raise floor to 3.11 or exclude from workspace). Test coverage to be expanded.

* ci: exclude azure-cosmos-memory from uv workspace resolution

The package depends on azure-cosmos-agent-memory which requires Python
>=3.11 and a prompty pre-release (>=2.0.0a9). Both are unsatisfiable
against the workspace's >=3.10 floor and pre-release policy, causing
uv sync to fail in every Python CI job. Exclude the package from the
shared workspace so it is resolved and tested as a standalone package.

* ci: fix code-quality failures for azure-cosmos-memory

- Strip trailing whitespace from package files (pre-commit trailing-whitespace hook)
- Exclude the package README from markdown-code-lint: the package is excluded
  from the uv workspace, so its README snippets import a module that is not
  installed in the workspace env and Pyright cannot resolve it

* Exclude azure-cosmos-memory README from markdown-code-lint task

* Address PR review comments on cosmos-memory context provider

- Wire credential into Cosmos and AI Foundry clients; let toolkit own
  DefaultAzureCredential when none supplied (remove dead import).
- Honor auto_extract=False by zeroing extraction/summary cadence thresholds.
- Skip whitespace-only conversation turns and store stripped content.
- Show confidence 0.0 and coerce confidence to float in _format_memories.
- Register both 'integration' and 'azure' pytest markers accurately.
- Fix duplicated install block in README.
- Update and extend unit tests for new credential wiring and fixes.

* Include azure-cosmos-memory in the uv workspace

Follow the github_copilot pattern for a package with a Python 3.11-only
dependency: lower requires-python to >=3.10 and gate azure-cosmos-agent-memory
behind a python_version >= '3.11' marker. Add a direct, gated prompty
pre-release dependency so the workspace's if-necessary-or-explicit prerelease
policy permits the toolkit's transitive prompty requirement. Guard the test
modules with pytest.importorskip so the 3.10 CI leg skips cleanly. Remove the
workspace exclude and the markdown-code-lint exclude, and regenerate uv.lock.

* Address review feedback on cosmos-memory provider

Rename provider parameters to match Agent Framework conventions:
foundry_endpoint (was ai_foundry_endpoint) and embedding_model/chat_model
(were *_deployment_name). Move DEFAULT_* to module-level constants, type
memory_types as a Literal, use DEFAULT_CONTEXT_PROMPT as the default value,
and add ProcessorConfig/CosmosMemorySettings TypedDicts. Resolve connection
settings via agent_framework load_settings with required-field validation,
replacing the manual getenv/raise blocks. Scope user_id/thread_id to the
provider state and drop the unpreventable first-turn warning.

Rewrite the samples around Agent (not raw SessionContext), provider-scoped
state, and session-id threading; use PEP 723 inline dependencies instead of a
samples dependency group; use a plain input() loop; remove the dead custom
processor stub. Update README/AGENTS for the renamed parameters and env vars.
Add a samples ruff per-file-ignores entry now that the package is linted in CI.

* Add emulator-backed vector search integration test

Bump azure-cosmos-agent-memory to >=0.2.0b2 (adds the embeddings/chat client
injection seam) and add tests/test_emulator.py: an integration (not azure)
suite that exercises real Cosmos vector search with a quantizedFlat index
against a local Cosmos DB emulator, using deterministic in-memory fakes for
embeddings and chat so no Azure AI Foundry account or LLM is required.

To run on a stock emulator the fixture strips the toolkit's full-text index
(the provider only does pure vector search) and requests provisioned autoscale
throughput instead of serverless. The suite skips cleanly when no emulator is
reachable.

* Fix CI typing and package checks for azure-cosmos-memory

The package recently joined the uv workspace, so its source and tests are now covered by the Test Typing Checks and Package Checks gates for the first time.

tests: rename stale constructor kwargs to the current provider API (foundry_endpoint/embedding_model/chat_model); use a typed _STUB_AGENT for the unused agent param so pyright/pyrefly/ty/zuban all accept it; make processor_config values ints; assert non-None memory_client in the emulator tests.

source: relax reportUnknown*/reportOptional* for this package only (the toolkit ships no py.typed; mirrors the hosting-telegram precedent); decouple the conditional toolkit import from the annotation type; use settings.get(); fix memory_types list invariance; drop a redundant None guard; read role via getattr.

* Apply pyupgrade: single-arg AsyncGenerator in test_integration

* Make Cosmos memory extraction drain transparently on provider exit

The provider now drains in-flight background memory extraction in __aexit__, so applications no longer need to call flush() in their own control flow; the client's close() would otherwise cancel pending extraction tasks. flush() is hardened against clients that expose no usable background-task registry.

sample: interactive_chat reads input via asyncio.to_thread so the event loop stays free and background extraction runs during the session; removes the manual flush now that the provider drains on exit.

tests: add explicit transparent-extraction integration tests (emulator: after_run schedules extraction and __aexit__ drains it; live Azure: a fact is extracted and recalled in a later session with no manual flush). Emulator tests reuse a single fixed database to avoid exhausting the emulator's partition budget across runs.

* Add custom extraction-prompt seam and sample to cosmos-memory provider

Adds a prompts_dir option to CosmosMemoryContextProvider that points the Agent Memory Toolkit pipeline at a caller-supplied directory of Prompty templates, so callers can override extract_memories.prompty to control what the extraction LLM produces. The toolkit exposes no public prompts-directory seam, so the provider contains the one internal touch (swapping the pipeline's template loader after the store connects); applies to both provider-built and supplied clients.

sample: interactive_chat_custom_extraction.py - the interactive chat wired with a custom coding-assistant extraction rubric. It derives a complete prompts directory at runtime (copies the bundled templates and augments extract_memories.prompty) so it stays schema-compatible with the installed toolkit.

tests: unit tests assert the provider redirects the pipeline loader only when prompts_dir is set; an emulator integration test proves end to end that a unique marker in a custom extract_memories.prompty reaches the extraction LLM call.

* docs: document prompts_dir custom-extraction seam in cosmos-memory README

Replaces the stale, non-functional CustomMemoryProcessor snippet with the working prompts_dir approach, lists the new interactive_chat_custom_extraction.py sample, and corrects the interactive-sample feature list.

* Address review: rename _new_session, drop defensive toolkit import guard

Sample (comment): rename _new_thread to _new_session in both interactive samples (a new session is the new thread).

Provider (comment): replace the _memory_toolkit_available flag + __init__ ImportError guard with a plain guarded import that re-raises a clear ImportError, matching the github_copilot package's pattern for its 3.11-only SDK. Kept requires-python >=3.10 (bumping this one workspace member to 3.11 would force the entire uv workspace lock floor to 3.11). Tests now run importorskip before importing the package, mirroring github_copilot.

* Pass cadence via cadence_thresholds instead of mutating os.environ

* Mark package alpha and drop private naming in samples

* Require Python 3.11 and inject user summary as untrusted context

* CI: exclude azure-cosmos-memory from uv sync on Python 3.10

* Re-trigger CI (flaky external link check)

* Require chat/embedding models instead of silent defaults

* Fix pyright: narrow resolved chat/embedding models to str

---------

Co-authored-by: Theo van Kraay <thvankra@microsoft.com>
2026-07-20 09:44:11 +00:00
Evan Mattson c66bb39ea2 Normalize durable workflow inputs (#7205) 2026-07-20 08:33:20 +00:00
Yufeng He 7c6b1e975f Python: fix compaction token count inflating non-ASCII text (#7124)
TokenBudgetComposedStrategy estimates tokens by feeding a JSON-serialized
message to the tokenizer, but _serialize_message() used ensure_ascii=True.
That escapes non-ASCII text into \uXXXX sequences, so CJK and other
non-Latin content is token-counted as the escape sequences rather than the
characters the model actually sees, inflating the estimate (~1.6x for mixed
Japanese, more for pure CJK) and skewing compaction/token-budget decisions.

Serialize with ensure_ascii=False, matching the ensure_ascii=False already
used elsewhere in this module. Only affects token estimation; the serialized
string is never stored or transmitted.

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-07-19 12:44:40 +00:00
Yufeng He 0d2925037d Python: preserve explicit null arguments in auto function calling (#7108)
* Python: preserve explicit null arguments in auto function calling

FunctionTool.invoke dumped validated arguments with model_dump(exclude_none=True),
which strips any argument the model set to null. A required nullable parameter
(e.g. unit: Literal["C","F"] | None) that the model deliberately sets to null was
therefore dropped, and the function failed to invoke on the missing argument.

Use exclude_unset instead: keep the arguments the model actually provided (null
included) and omit only the ones it left out, so the function's own defaults still
apply. Because the input model is generated from the function signature, its field
defaults match the signature defaults, so omitted optionals are unchanged.

Fixes #5934

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>

* Python: extend null-arg fix to the auto function-calling path

The earlier change fixed FunctionTool.invoke, but _auto_invoke_function
(the path a model-emitted function_call actually takes) still ran
model_dump(exclude_none=True), so an explicit null for a required
nullable argument was still dropped and the call failed with a missing
argument. Switch it to exclude_unset to match invoke, and add a
regression test that drives _auto_invoke_function with an explicit null.

---------

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-07-19 12:43:41 +00:00
Eduard van Valkenburg b5e635ed4d Python: isolate hosted session snapshots (#7141)
* Python: isolate hosted session snapshots

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

Copilot-Session: 75d26ffd-7dc7-46b3-9966-9aaebb7b6bc3

* Python: avoid duplicate conversation snapshots

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

Copilot-Session: 75d26ffd-7dc7-46b3-9966-9aaebb7b6bc3

* added some notes in the docstring
2026-07-18 11:59:24 +00:00
Eduard van Valkenburg 1036fa7438 Python: docs: add self-hosting sample snippets (#7104)
* docs: add self-hosting sample snippets

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 636b7fb2-381a-4d62-b5cf-d029efa3ad20

* docs: use stable hosting sample ranges

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 636b7fb2-381a-4d62-b5cf-d029efa3ad20

* docs: address hosting sample review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 636b7fb2-381a-4d62-b5cf-d029efa3ad20

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-17 22:10:28 +00:00
Eduard van Valkenburg 62da382082 Python: Optimize shared serialization paths (#7165)
* Optimize core serialization paths

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

Copilot-Session: cda42f21-2500-4f78-a527-d6eeaeef922b

* Streamline AG-UI serialization

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

Copilot-Session: cda42f21-2500-4f78-a527-d6eeaeef922b

* Document shared serialization guidance

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

Copilot-Session: cda42f21-2500-4f78-a527-d6eeaeef922b

* Bound serialization protocol cache

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

Copilot-Session: cda42f21-2500-4f78-a527-d6eeaeef922b
2026-07-17 22:09:53 +00:00
Eduard van Valkenburg 3604ba70f6 Python: Normalize chat finish reasons (#7105)
* Python: Normalize chat finish reasons

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

Copilot-Session: 45b65bfa-8e36-47b0-99d9-ec58ec60ace1

* Preserve Copilot finish reasons

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

Copilot-Session: 45b65bfa-8e36-47b0-99d9-ec58ec60ace1

* fix claude finish reasons
2026-07-17 22:05:36 +00:00
Marco Minerva 3ab2630243 .NET: Refactor Workflows MessageMerger to preserve message order and structure (#6826)
* Refactor MessageMerger to preserve message order

Refactored MessageMerger to delegate update grouping and merging to M.E.AI, preserving the correct order and structure of assistant messages, especially for reasoning content without message IDs. Removed per-message bucketing and CreatedAt-based sorting. Added tests to verify message order and correct merging of reasoning and text updates.

* Set CreatedAt from merged responses preservation of original message timestamps during merging.

* Set merged message CreatedAt to current UTC time

Removed logic for tracking unique creation times and now always assign DateTimeOffset.UtcNow to the merged response's CreatedAt property. This simplifies timestamp handling during message merging.

* Refactor MessageMerger id-less folding logic

Refactored MessageMerger to fold identifierless reasoning segments into the following id'd message at the flattened-message level, ensuring correct merging across response buckets (fixes #6329). Updated ComputeMerged to merge id-less messages with the next message of the same role. Removed redundant per-bucket folding logic. Added unit tests to verify correct folding behavior and role matching.

* Remove unused property

 Removed the unused Role property from MessageMergeState for code cleanliness.

* Refactor MessageMerger to iterate backward for merging

Changed MessageMerger to iterate messages in reverse order, ensuring all consecutive messages without IDs preceding a message with an ID are merged correctly. Updated merging logic, index handling, and comments to reflect this new approach.

* Update code comment to better reflect its behavior.

---------

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2026-07-17 17:10:14 +00:00
Eduard van Valkenburg bc59c72170 Python: Add A2A hosting helpers (#7050)
* Python: Add A2A hosting helpers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d6987cd-1b67-4ba1-8b54-3c50da6e7607

* Python: Preserve final A2A streaming output

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d6987cd-1b67-4ba1-8b54-3c50da6e7607

* Python: Clarify A2A conversion boundary

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

Copilot-Session: 5d6987cd-1b67-4ba1-8b54-3c50da6e7607

* Python: Document A2A sample auth boundary

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

Copilot-Session: 5d6987cd-1b67-4ba1-8b54-3c50da6e7607

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-17 16:58:39 +00:00
ssccinng d5f2c77b35 .NET: Honor terminal workflow outputs in Workflow.AsAIAgent responses (#6212)
* Fix workflow agent terminal output responses

* Address workflow agent response review feedback

---------

Co-authored-by: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com>
2026-07-17 15:36:26 +00:00
VectorPeak 6afae2f9b4 Python: raise ValueError for malformed data URIs (#6916)
* Python: raise ValueError for malformed data URIs

* Address malformed data URI review feedback

---------

Co-authored-by: VectorPeak <VectorPeak@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-07-17 11:40:20 +00:00
Benke Qu cad81923e3 Python: fix: concurrent_agents sample incorrectly treats output as list[Message] (#6548)
* Python: fix: concurrent_agents sample treats output as list[Message] but it is AgentResponse

The default ConcurrentBuilder aggregator yields AgentResponse, not
list[Message]. The sample incorrectly cast the output to list[Message]
and iterated it directly. Fix by checking isinstance(output, AgentResponse)
and iterating output.messages instead.

* fix: remove warning print per review feedback

* fix: address review - remove unused Message import, update docs and sample output

- Remove unused Message import
- Update docstring: default aggregator yields AgentResponse objects, not list[Message]
- Fix sample output: remove user prompt entry (aggregator returns only assistant messages)
- Renumber sample output entries (researcher=01, marketer=02, legal=03)

---------

Co-authored-by: Benke Qu <bequ@microsoft.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-07-17 11:38:04 +00:00
Ahmed Muhsin 5ab8877ba5 Python: HITL respond-URL addressing from inside workflows (#7001)
* feat(durabletask): surface HITL respond-URL addressing to workflow executors

Let a workflow notify a human reviewer (e.g. email an approval link) from inside the
graph, without the caller threading the instanceId/requestId by hand.

- durabletask: the orchestrator injects host_context {instance_id, workflow_name,
  request_path_prefix} into each activity input; CapturingRunnerContext surfaces it as
  host_metadata. No new core API.
- azurefunctions: WorkflowHitlContext.from_context(ctx) builds the canonical
  respond/status URLs (returns None in-process so callers degrade gracefully).
  Re-exported through the agent_framework.azure lazy namespace.
- Nested sub-workflows: the address context (root instance + workflow name +
  accumulated {executor}~{ordinal}~ prefix) propagates down call_sub_orchestrator via a
  new SUBWORKFLOW_ADDRESS_KEY marker, so an executor at any depth builds a URL that
  targets the addressable top-level instance with a qualified request id. The per-child
  ordinal matches the read-side enumerate() index used by the status/respond endpoints.
  The marker is stripped from untrusted input alongside SUBWORKFLOW_INPUT_KEY
  (confused-deputy / info-leak guard).
- Samples 12 and 13 reworked into the retry-safe two-step notify pattern: the emitter
  generates an explicit request id and a downstream NotifyExecutor builds the URL and
  notifies, so failed upstream retries never produce a dead link.

Tests: unit coverage for the metadata round-trip, address/ordinal agreement (fan-out at
depth and nested prefix accumulation), marker stripping, and URL building; integration
tests assert the helper-built URL equals the server respondUrl and resumes the run, for
both the flat (12) and nested (13) samples.

* refactor(durabletask): read back request_info id instead of generating one in samples

Add WorkflowHitlContext.pending_request_id(ctx), an async helper that returns the
id request_info just generated (read from the runner context's pending request-info
events). This works on any host via the core RunnerContext protocol method, so it
needs no core change.

Samples 12 and 13 now call request_info() and read the id back to forward to the
NotifyExecutor, instead of minting a uuid by hand and passing request_id=. The
read-back happens in the same activity execution that generated the id, so the
pending request event and the notify message still commit together with the same id
(retry-safe; failed upstream retries notify no one).

* docs(azurefunctions): document request_info id read-back and notify safety

Tighten pending_request_id docstring to require calling it immediately after request_info, and explain why that is safe on the durable host (each executor runs in its own activity with its own runner context, so the pending set only holds this executor's requests and the newest is the one just emitted). Document the two-step notify pattern in the 12 and 13 sample READMEs, including the downstream-notifier retry safety and the nested address-prefix propagation.

* fix(python): resolve ty typing error and address PR review comments

- test_subworkflow_orchestration: replace the mypy-only type:ignore[arg-type] with a cast so the ty checker passes too (the other four checkers already honored the ignore).

- samples 12/13 README: guard the notify snippet against None before build_respond_url to match the documented graceful-degradation behavior.

- integration tests 12/13: reword comments that implied request_info now generates an explicit uuid4; it generates the id internally by default.

* fix(python): honor configurable Functions route prefix and address HITL PR review

- Resolve the route prefix from host.json (extensions.http.routePrefix, default api) in a new azurefunctions _routes module, used by both the server endpoints and WorkflowHitlContext, so a custom or empty routePrefix no longer 404s respond/status URLs (was hardcoded /api/ in four places).

- Extract respond/status URL construction into one shared builder called from _app.py and _hitl_context.py, removing the sync-by-test duplication.

- Broaden loopback detection (localhost, 127.0.0.0/8, 0.0.0.0, ::1, [::1]) via a _is_loopback helper so local links use http.

- Pin the host_context key names as shared constants in durabletask so producer and azurefunctions consumer cannot drift.

- Reword a stale base_url comment to reference WEBSITE_HOSTNAME.

- Add unit tests for the route module and loopback handling.

* refactor(azurefunctions): derive server-side HITL URLs from the request URL

The run and status endpoints now derive the base URL and route prefix from the incoming request URL (the value the host actually routed) via split_request_url, so the caller-visible respond/status URLs no longer depend on reading host.json on the server. The in-workflow helper keeps reading host.json since it has no request context. Replaces strip_route_prefix and updates its tests.

* test(durabletask): enforce sub-workflow ordinal and read-index agreement

Extract the read-side subworkflows grouping into a shared _index_subworkflows helper (used by the orchestrator) and add test_readside_index_matches_dispatch_ordinal, which round-trips a fan-out through that helper and asserts subworkflows[executor][ordinal] resolves to the child the dispatch stamped that ordinal onto. Turns the previously comment-only write-ordinal / read-index invariant into a shared, CI-enforced one.

* fix(azurefunctions): suppress bandit B104 on loopback host set
2026-07-16 22:06:25 +00:00
Jose Luis Latorre Millas f4e49958f3 samples: add AgentMemory (Neo4j-agent memory reimplemented in NET ) shopping assistant sample (#7096)
* samples: add Neo4j Shopping Assistant (standalone, published AgentMemory 1.0.1)

The .NET port of the official Neo4j Agent Memory "retail assistant" example
(neo4j-labs/agent-memory examples/microsoft_agent_retail_assistant, referenced
from the Learn integration page), which is currently Python-only.

Wires Neo4jMemoryContextProvider (AIContextProvider), MemoryToolFactory
memory tools, and a ProductCatalog of retail tools over a Neo4j :Product
graph, via the published AgentMemory + AgentMemory.AgentFramework 1.0.1
NuGet packages.

Lives at the repo root rather than under dotnet/samples/: that tree is
.NET 10 + Central Package Management + Microsoft.Agents.AI ~1.13 with
source ProjectReferences, while AgentMemory currently targets net9.0 +
Microsoft.Agents.AI 1.9.0. A repo-native version needs AgentMemory bumped
to track the newer Agents.AI/Extensions.AI line first. Cross-linked from
dotnet/samples/02-agents/AgentWithMemory/README.md as a "See also" entry,
same pattern already used for the cross-folder Custom Memory Implementation
link.

Verified: dotnet build succeeds (0 warnings, 0 errors) against the published
packages, proving the AgentMemory public surface is package-consumable.
Matches sibling AgentWithMemory samples' conventions (BOM + copyright file
header on .cs files, README sections: Features Demonstrated / Prerequisites
/ Environment Variables / Run the Sample / Expected Output).

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

* samples: move Neo4j shopping assistant into AgentWithMemory as Step06

Relocates the standalone shopping-assistant sample from the repo root into
dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory,
following that folder's naming/README/solution conventions. Renames its identity
from "Neo4j" to "AgentMemory" (the library it actually demonstrates) since this
is a community .NET port, not an officially recognized Neo4j integration - Neo4j
is still referenced where it's a genuine technical detail (the graph backing
store, env vars, Cypher). Adds empty Directory.Build.props/targets markers so it
stays isolated from the repo's net10.0/CPM build, and registers it (skipped, like
the Mem0 sample) in the CI sample-verification list since it needs a live Neo4j
instance.

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

* Update dotnet/samples/02-agents/AgentWithMemory/README.md

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

* Update dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory.csproj

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

* cleanup :)

* DefaultAzureCredential warning

* fixes - simplification userId

* minor doc fix

* NU1015 fix

* PR review fixes-improvements

* Bump AgentMemory to 1.2.0, let the context provider surface memory tools

WithMemoryOwnerScoping(sp) (1.1.0) already removed the need to manually
wrap agent.RunAsync in ownerContext.BeginOwnerScope(userId). This picks
up 1.2.0's ExposeMemoryToolsFromContextProvider option, so
Neo4jMemoryContextProvider now appends the memory tools to AIContext.Tools
itself on every model call — no more separate MemoryToolFactory wiring,
AIContextProviders = [memoryProvider] is enough.

Addresses westey-m's PR review suggestion.

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

* improvements according to pr review comments

* Fix CI: use plural TargetFrameworks to actually restrict this sample to net10.0

Directory.Build.props sets a repo-wide TargetFrameworks (plural) list
before this project's own properties are evaluated, and the SDK decides
multi-targeting from that plural property at Sdk.props time. The prior
singular TargetFramework=net10.0 override didn't take effect early
enough, so restore still ran against net9.0/net8.0/netstandard2.0/net472
too - frameworks the published AgentMemory 1.2.0 packages don't support
(NU1202), plus surfaced an OpenTelemetry.Api advisory as an error
(NU1902) since TreatWarningsAsErrors is on repo-wide.

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

* Fix CI: pin OpenTelemetry.Api to unblock NU1902 audit failure

The sample opts out of central package management, so it was pulling in
OpenTelemetry.Api 1.12.0 transitively (via Microsoft.Agents.AI), which has
a known moderate-severity vulnerability (GHSA-g94r-2vxg-569j). The repo
treats NuGet audit warnings as errors, so restore failed outright and took
down every dotnet-build matrix leg plus check-format.

Pinned OpenTelemetry.Api to 1.15.3, matching Directory.Packages.props.
With restore succeeding, previously-masked analyzer/format issues surfaced
and are fixed too: RCS1118 (const local for immutable Cypher queries),
CA1859 (List<IRecord> param instead of IReadOnlyList<IRecord>), and IDE1006
naming violations (s_seed field prefix, PascalCase Cypher/Shopper consts).

Verified locally with the same mcr.microsoft.com/dotnet/sdk:10.0 image CI
uses: dotnet build --warnaserror and dotnet format --verify-no-changes both
pass clean, and a full solution build completed ~24 min with zero errors.

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

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
2026-07-16 15:39:23 +00:00
Eduard van Valkenburg 5282c158aa .NET: Fix LocalCodeAct validation and package checks (#7138)
* Fix LocalCodeAct validation and package checks

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

Copilot-Session: aecf332f-b940-41a7-ac3a-6fbbe9892141

* Address LocalCodeAct alias review feedback

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

Copilot-Session: aecf332f-b940-41a7-ac3a-6fbbe9892141
2026-07-16 15:17:52 +00:00
feiyun0112 dde7635760 .NET: [Feature]: .NET Improve ChatClientAgentSession constructor (#7142)
* .NET: [Feature]: .NET Improve ChatClientAgentSession constructor

* Potential fix for pull request finding

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

* test: make deserialize test actually reproduce issue #7109

VerifyDeserializeWithWhenWritingNullOptions passed against both the old
and the fixed constructor, so it did not guard against the regression.

The bug only reproduces when required constructor parameters are respected
(the issue uses RespectRequiredConstructorParametersDefault=true). With
WhenWritingNull a null conversationId is omitted from the JSON, and STJ then
throws 'missing required properties including: conversationId' because the
constructor parameter had no default value.

Adding RespectRequiredConstructorParameters = true to the test options makes
the test red against the parameter-without-default constructor and green with
the default-valued constructor parameters, so it now protects the fix.

---------

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-07-16 15:06:37 +00:00
King Star 85c00fc55b .NET: preserve HeadTailBuffer UTF-8 order (#7128)
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2026-07-16 10:03:43 +00:00
westey f19a129b55 Graduate FileMemoryProvider (#7114) 2026-07-16 09:38:59 +00:00
westey a376577263 Gradudate FileMemoryProvider (#7113) 2026-07-15 22:57:21 +00:00
Tao Chen b2549337ff Python: Best effort to serialize tool def to Json for observability (#7029)
* Best effort to serialize tool def to Json

* Fix formatting

* Fix tests

* Optimize json serialization

* Best effort: Add secret filtering

* Remove frozen set and only convert required fields

* Fix tests

* Address comments

* Fix typing
2026-07-15 20:43:36 +00:00
Peter Ibekwe 05834b56e3 Fix message ordering in workflow-hosted agents (#7123) 2026-07-15 19:40:47 +00:00
Roger Barreto 42ae534a07 CI: resolve PR author in community team check (#7129)
* CI: resolve PR author in community team check

pull_request_target events expose the author on payload.pull_request, not payload.issue. Read that field first and fall back to pulls.get so limit-community-prs no longer calls issues.get and fails with 401.

* Docs: clarify issueNumber accepts PR numbers
2026-07-15 18:14:02 +00:00
Evan Mattson a17102f9f5 Harden manual integration test trust boundary (#7081)
* Harden manual integration test workflow

Require two write-capable approvals for the exact PR head SHA, pin all targets to immutable commits, and narrow secret and OIDC access.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b8c7d85f-7576-4fc7-a7b5-c77833344088

* Require integration workflow credentials

Declare credentials consumed by the reusable integration workflows as required while retaining the explicitly optional Foundry models key.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b8c7d85f-7576-4fc7-a7b5-c77833344088

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-15 17:54:52 +00:00
westey b3f2e53923 .NET: [BREAKING] Graduate ToolApprovalAgent and add ToolAutoApprovalRuleContext (#7107)
* Graduate ToolApprovalAgent and introduce tool auto approval context

* Address PR comments
2026-07-15 09:21:12 +00:00
Chris Brown b5300fe0c0 Python: fix per-run additional_beta_flags leaking into Anthropic request kwargs (#7060)
* Python: fix per-run additional_beta_flags leaking into Anthropic request kwargs

_prepare_options copied every key from the caller-supplied options dict
into run_options except "instructions" and "response_format". A
per-run additional_beta_flags value is correctly folded into the betas
set by _prepare_betas, but the raw key was never excluded, so it
survived into run_options and was forwarded straight through to
AsyncMessages.create(), which rejects it with TypeError: got an
unexpected keyword argument 'additional_beta_flags'. Add it to the
exclusion set alongside the other framework-level keys.

Fixes #5764

* Exclude additional_beta_flags from filtered_kwargs too

Copilot's review on the original fix pointed out the exclusion only
covered the options-dict copy, not kwargs passed directly to
_prepare_options — so additional_beta_flags supplied as a raw kwarg
would still leak through and reproduce the same TypeError. Add the
same exclusion to filtered_kwargs for consistency, with a regression
test covering the kwarg path.

---------

Co-authored-by: Chris Brown <albatrossflyon1@gmail.com>
2026-07-14 23:02:55 +00:00
Evan Mattson c35a63ed8d Python: feat: cross-session origin attribution on context messages (#7041)
* Python: feat: cross-session origin attribution on context messages

Add an optional origin_session_id parameter to SessionContext.extend_messages
that propagates into the existing _attribution payload on
Message.additional_properties. Downstream context observers can use it to
detect when a provider injects content stored under a different session than
the requesting one.

Populate the field from the harness memory consolidation pipeline
(_harness/_memory.py) when injected topics include contributions from
sessions other than the current one. Add a self-contained sample observer
under samples/02-agents/context_providers/cross_session_observer.py
demonstrating how to subscribe to the signal.

Backward-compatible: omitting the parameter preserves the existing
attribution shape exactly. Tests added in test_sessions.py and
test_harness_memory.py cover the new parameter, the harness cross-session
case, and the same-session case.

Motivated by Dai et al., Stateful Agent Backdoor (arXiv:2605.06158, May
2026), which specifically surveys MAF in section 6.1 / Table 10.
See #5914 for design discussion.

Surfaced during independent audit conducted by @finnoybu (Ken Tannenbaum, AEGIS Initiative); [MEDIUM, python/packages/core].

* Address cross-session attribution review feedback

* Address follow-up review feedback

* Address follow-up review comments

* Address origin attribution review feedback

---------

Co-authored-by: finnoybu <21694570+finnoybu@users.noreply.github.com>
2026-07-14 17:34:52 +00:00
Rince Yuan 47cd0a508d docs/.NET: fix typos in XML doc comments, ADR docs, and test comments (#7085)
- Fix double period in AnthropicClientExtensions.cs XML param docs (lines 23, 77)
- Fix double period in IScopedContentProcessor.cs XML param doc (line 20)
- Fix 'similar the the' -> 'similar to the' in ADR 0009 (line 1092)
- Fix 'reponse' -> 'response' in ADR 0001 (line 142)
- Fix 'retreive' -> 'retrieve' in ChatClientAgentTests.cs (line 467)
- Remove leftover template placeholder from ADR 0001 and 0006 frontmatter

Co-authored-by: j-zhangyiyuan <j-zhangyiyuan@microsoft.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: Giles Odigwe <79032838+giles17@users.noreply.github.com>
2026-07-14 17:06:46 +00:00
Giles Odigwe 56e9a8f74c Python: Make foundry toolbox MCP skills sample self-contained (#7099)
* Python: Make foundry toolbox MCP skills sample self-contained

Rework sample 12 (foundry_toolbox_mcp_skills) so users can build it from
zero with azd, mirroring samples 04 and 09:

- Bundle two single-file SKILL.md skills (support-style, escalation-policy)
  and a skills-only toolbox.yaml (with one connectionless code_interpreter
  tool, required by `azd ai toolbox create`).
- Rewrite the README as an azd-native, from-zero guide (create skills ->
  create toolbox -> set TOOLBOX_ENDPOINT -> run) and fix the stale
  MCPSkillsSource API description to match main.py.
- Switch config from TOOLBOX_NAME to the versioned TOOLBOX_ENDPOINT
  (.env.example, agent.yaml, agent.manifest.yaml); add .azdignore.

Also enable the sample to run unattended behind ResponsesHostServer:

- Forward disable_load_skill_approval / disable_read_skill_resource_approval
  / disable_run_skill_script_approval from FoundryToolbox.as_skills_provider()
  to the underlying SkillsProvider, so load_skill needs no approval round-trip
  (the Responses host runs without an AgentSession, which the default approval
  flow requires). main.py now uses as_skills_provider(disable_load_skill_approval=True).
- Add unit tests covering the default and overridden approval behaviour.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4f14f83d-1868-45c1-be1a-12f49a58ac36

* Python: Address PR review on toolbox MCP skills sample

- Remove the unused parameters section from agent.manifest.yaml (TOOLBOX_ENDPOINT
  is supplied via environment_variables, matching sample 04).
- README: state the sample is self-contained directly instead of contrasting
  with the C# sample.
- README: describe skill discovery behaviour without naming the internal
  MCPSkillsSource class.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4f14f83d-1868-45c1-be1a-12f49a58ac36

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-14 17:03:26 +00:00
westey ba0ad2d1d2 Gradudate ToolApprovalMiddleware (#7106) 2026-07-14 11:16:10 +00:00
Evan Mattson b123480b65 Python: Fix AG-UI workflow handoff replay results (#7102)
* Fix AG-UI workflow handoff replay results

Decisions:
- Reconcile finalized function results only for call IDs exposed in the current run, and skip results already emitted or never exposed.
- Treat message-derived function results as workflow responses only when their IDs match pending interrupts.

Files:
- Updated _workflow_run.py reconciliation and resume filtering.
- Added runner and public two-turn handoff acceptance coverage.
- Expanded finalized-response call-ID, privacy, and deduplication tests.

Verification:
- uv run poe test -P ag-ui
- uv run poe syntax -P ag-ui -C
- uv run poe pyright -P ag-ui
- uv run poe test-typing -P ag-ui

Notes:
- No blockers. The handoff sample remains unchanged; local PRD and issue files are not included.

* Prevent duplicate AG-UI workflow tool results

* Preserve finalized AG-UI tool results

* Use AG-UI text emission controls
2026-07-14 09:29:46 +00:00
Evan Mattson 4c0d9ed43c Python: .NET: Consolidate Dependabot dependency updates (#7103)
* Bump Anthropic from 12.31.0 to 12.35.1

---
updated-dependencies:
- dependency-name: Anthropic
  dependency-version: 12.35.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* #7070 Bump Anthropic.Foundry from 0.6.0 to 0.7.1

* Bump astral-sh/setup-uv from 7.6.0 to 8.3.2

Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 7.6.0 to 8.3.2.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/37802adc94f370d6bfd71619e3f0bf239e1f3b78...11f9893b081a58869d3b5fccaea48c9e9e46f990)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 8.3.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

* Bump actions/cache/save from 5.0.5 to 6.1.0

Bumps [actions/cache/save](https://github.com/actions/cache) from 5.0.5 to 6.1.0.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/27d5ce7f107fe9357f9df03efb73ab90386fccae...55cc8345863c7cc4c66a329aec7e433d2d1c52a9)

---
updated-dependencies:
- dependency-name: actions/cache/save
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

* Bump github/codeql-action/analyze from 4.35.5 to 4.37.0

Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.35.5 to 4.37.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/9e0d7b8d25671d64c341c19c0152d693099fb5ba...99df26d4f13ea111d4ec1a7dddef6063f76b97e9)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* Bump softprops/action-gh-release from 2.6.2 to 3.0.1

Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.6.2 to 3.0.1.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/3bb12739c298aeb8a4eeaf626c5b8d85266b0e65...718ea10b132b3b2eba29c1007bb80653f286566b)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

* Bump dorny/paths-filter from 4.0.1 to 4.0.2

Bumps [dorny/paths-filter](https://github.com/dorny/paths-filter) from 4.0.1 to 4.0.2.
- [Release notes](https://github.com/dorny/paths-filter/releases)
- [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dorny/paths-filter/compare/fbd0ab8f3e69293af611ebaee6363fc25e6d187d...7b450fff21473bca461d4b92ce414b9d0420d706)

---
updated-dependencies:
- dependency-name: dorny/paths-filter
  dependency-version: 4.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

* Bump astral-sh/setup-uv in /.github/actions/python-setup

Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 6.8.0 to 8.3.2.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/d0cc045d04ccac9d8b7881df0226f9e82c39688e...11f9893b081a58869d3b5fccaea48c9e9e46f990)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 8.3.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

* Align workflow action versions

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 09:18:27 +00:00
t-anjan 1c0082721c Python: Fix structured value parsing for split text chunks (#6990)
* Fix structured value parsing for split text chunks

* Potential fix for pull request finding

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

* Fix structured value parsing for split text chunks

---------

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-14 08:39:11 +00:00
S3rj 6c0950adeb fix: clarify require_confirmation docstring to reflect confirm_changes HITL gating (#6884)
Co-authored-by: Sergey Borisov <sergey.borisov@dataimpact.io>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-07-14 08:01:58 +00:00
Hasan Ghomi f1ba16e3fd Python: Fix Magentic manager duplicating conversation history (#6297)
* Python: Fix Magentic manager duplicating conversation history

_complete() reused one persistent AgentSession, so the default history provider
re-injected prior turns on top of the full prompt the manager already rebuilds
each call — duplicating task/facts/plan and compounding every round. Use a fresh
session per call; keep self._session only for
checkpointing. GroupChatOrchestrator is unaffected. Add a regression test and
update the session-propagation test.

* Python: Clean up Magentic manager per Copilot review (drop dead _session)

---------

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-07-14 08:01:18 +00:00
Giles Odigwe cba77e3cd0 Python: quiet A2AExecutor logging for unmapped content types (#7034)
* Python: quiet A2AExecutor logging for unmapped content types

Tool-use responses include function_call/function_result content that the A2A executor does not surface, causing a WARNING per tool call. Log these at DEBUG and skip instead, matching the outbound content-conversion convention used across the Python chat clients.

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

Copilot-Session: ee74cc55-44df-4fcf-b38f-1f79f2600dfc

* Address PR review: assert debug log args and drop redundant cast

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

Copilot-Session: ee74cc55-44df-4fcf-b38f-1f79f2600dfc
2026-07-14 07:44:31 +00:00
Eduard van Valkenburg 7ca8bb55b6 Python: Add Telegram hosting helpers and samples (#7047)
* Python: Add Telegram hosting helpers

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

Copilot-Session: 5d6987cd-1b67-4ba1-8b54-3c50da6e7607

* Python: Exclude Telegram samples from aggregate typing

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

Copilot-Session: 5d6987cd-1b67-4ba1-8b54-3c50da6e7607

* Python: Address Telegram helper review feedback

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

Copilot-Session: 5d6987cd-1b67-4ba1-8b54-3c50da6e7607

* Python: Serialize Telegram webhook sessions

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

Copilot-Session: d76a9c32-d170-426d-a64f-b70958b08b12
2026-07-14 07:40:12 +00:00
westey d93fc2dd74 .NET: [BREAKING] Harness: Switch FileAccess to opt-in (#7093)
* Switch FileAcessProvider on Harness to opt-in

* Address PR comment

---------

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-07-14 07:08:33 +00:00
Nick Brady 54617557e6 Update Foundry branding (#6999)
Replace user-facing Azure AI Foundry branding with Microsoft Foundry across docs, samples, comments, and display text while preserving technical identifiers.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-07-14 06:44:26 +00:00
Syed Osama Ali Shah df198005fd Python: fix: preserve function-call name when merging streaming deltas (#6809)
* fix: preserve function-call name when merging streaming deltas

`Content._add_function_call_content` built the merged name with
`getattr(self, "name", getattr(other, "name", None))`. Because
`Content.__init__` always sets `self.name` (defaulting to `None`), the
attribute is never missing, so the `getattr` default is never consulted
and `other.name` is ignored. When two function_call contents are merged
and only the second carries the name -- e.g. a streaming delta where the
function name arrives after the first chunk -- the name was silently
dropped.

Use the same "either side" pattern already used for the sibling
`exception` field on the next line: `getattr(self, "name", None) or
getattr(other, "name", None)`. Extend the existing merge test to cover
the late-name and both-None cases.

* test: construct nameless function-call deltas via Content(...) directly

Per review (pyright `reportArgumentType`): `Content.from_function_call`
annotates `name: str`, so passing `name=None` to model a streaming delta
with no name yet tripped the typing gate. Build those nameless deltas
with the `Content("function_call", ...)` constructor instead (its `name`
param is `str | None`) — the factory just wraps that same constructor, so
the runtime objects and the merge assertions are unchanged.

---------

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-07-14 06:36:57 +00:00
westey 0ceca9a76a Add name collision warnings for auto-approvals (#7090)
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-07-14 06:35:38 +00:00
westey 23977a6045 .NET: Add name collision warnings for auto-approvals (#7089)
* Add name collision warnings for auto-approvals

* Address PR comments
2026-07-14 06:32:38 +00:00
Evan Mattson 18b03ea487 Python: adjust checkpoint encoding handling (#6579)
* Adjust checkpoint encoding handling

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

* Refine checkpoint encoding handling

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

* Adjust checkpoint dict encoding

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

* Clarify checkpoint unpickler blocked globals

* Preserve typed HITL requests in durable activities

* test: skip flaky durabletask multi-turn integration test

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-14 15:21:12 +09:00
Evan Mattson 56c4425db2 Python: bridge AG-UI request state and session continuity (#7084)
* Python: bridge AG-UI request state into sessions

Decisions:
- Project resolved AG-UI Shared State into the per-run AgentSession without typed restoration.
- Preserve existing local/service session identifiers and keep AG-UI state out of provider metadata.

Files changed:
- packages/ag-ui/agent_framework_ag_ui/_agent_run.py
- packages/ag-ui/tests/ag_ui/test_endpoint.py

Verification:
- uv run poe check -P ag-ui
- uv run poe test -P ag-ui (912 passed)

Notes:
- Scoped cross-run Session Continuation State remains for the next dependent issue.

* Python: persist scoped AG-UI session continuity

Decisions:
- Store private Session Continuation State atomically in scoped thread snapshots and restore it through the core AgentSession contract.
- Exclude Shared State keys, all HistoryProvider buckets, and tool approval state; request overlays evict colliding private values.
- Finalize interrupted response streams before snapshotting so provider after_run mutations are included.

Files changed:
- packages/ag-ui/agent_framework_ag_ui/_agent_run.py
- packages/ag-ui/agent_framework_ag_ui/_snapshots.py
- packages/ag-ui/tests/ag_ui/test_endpoint.py
- packages/ag-ui/tests/ag_ui/test_snapshots.py
- packages/ag-ui/AGENTS.md

Verification:
- uv run poe test -P ag-ui (921 passed)
- uv run poe check -P ag-ui
- uv run poe typing -P ag-ui

Notes:
- Lifecycle, isolation, and broader storage guidance remain for the next dependent issue.

* Python: document AG-UI session continuity lifecycle

Decisions:
- Keep scoped thread snapshots as the single reset and continuity boundary, with missing request Shared State preserving private continuation.
- Document trusted typed-restoration storage, State Authorities, custom-store round trips, and one-active-run last-writer-wins consistency.
- Verify failure, hydration privacy, scope/thread isolation, and reset mechanics through public endpoint and store seams.

Files changed:
- packages/ag-ui/README.md
- packages/ag-ui/tests/ag_ui/test_endpoint.py
- packages/ag-ui/tests/ag_ui/test_snapshots.py

Verification:
- uv run pytest -q <focused lifecycle tests> (6 passed)
- uv run poe test -P ag-ui
- uv run poe syntax -P ag-ui -C
- uv run poe typing -P ag-ui
- uv run poe check -P ag-ui
- uv run poe markdown-code-lint

Notes:
- No runtime capability probe, secondary state store, locking, or configuration flag was added.
- No blockers remain for this lifecycle and guidance slice.

* Python: harden AG-UI session continuity

* Python: isolate AG-UI request state
2026-07-14 04:55:46 +00:00
Evan Mattson 13066cdf96 Python: Refine DevUI request logging (#7083)
* Refine DevUI request logging

* Address DevUI logging review feedback
2026-07-14 03:40:18 +00:00
westey f11cfd9d76 Switch FileAcessProvider on Harness to opt-in (#7094) 2026-07-14 02:26:59 +00:00
Evan Mattson 774fc94bd2 Enable manual issue triage (#7098) 2026-07-14 11:04:38 +09:00
Peter Ibekwe 4bac2c2c05 Python: Promote python declarative workflows to stable version (#7065)
* Promote python declarative workflows to stable version

* Updated changelog with PR detail.

* Updated to address pr comments.

* Remove changelog update
2026-07-13 22:30:21 +00:00
pratik wayase 43568f1ef2 Fix: coalesce reasoning deltas into single block when content.id is None (#6804)
python/packages/ag-ui/tests/ag_ui/test_run_commoclear
:wq
2026-07-13 21:11:22 +00:00
VectorPeak 52005ff17d Python: accept AG-UI state data URI parameters (#6905)
* Python: Accept AG-UI state data URI parameters

* Python: Handle invalid AG-UI state base64

---------

Co-authored-by: VectorPeak <VectorPeak@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-07-13 21:05:27 +00:00
pratik wayase a4e4a5a51c Python: Fix: Ollama parallel tool calls collide on same call_id (#6822)
* Fix: Ollama parallel tool calls collide on same call_id

* fix(ollama): use uuid4 for tool call IDs and support colons in tool names

---------

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-07-13 21:02:24 +00:00
Alireza Afzali c8fb491644 Python: docs: add env example files for durabletask samples (#5948)
* docs: add env example files for durabletask samples

* docs: clarify env example values and comments

* docs: set default Redis URL in streaming sample env example
2026-07-13 21:01:19 +00:00
westey e57f046d8a Graduate mode and todo providers (#7052) 2026-07-13 13:47:58 +00:00
westey c9b19e831f Gradudate mode and todo providers (#7053) 2026-07-13 13:47:53 +00:00
westey beb65b21a8 .NET: [BREAKING] Graduate message injection out of experimental (#7044)
* Remove experiemental flags for MessageInjection component

* Improve locking on message injection.
2026-07-13 11:26:01 +00:00
westey b3d523ee50 Python: [BREAKING] Fix harness before-strategy compaction under per-service-call persistence (#7055)
* Fix middleware ordering to ensure compaction runs

* Address PR comments

* Fix build issue
2026-07-13 09:51:00 +00:00
Copilot 6f38cb724d .NET: Enable Valkey NuGet package publishing (#7059)
* Initial plan

* Enable Valkey NuGet package publishing

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-13 09:31:49 +00:00
Tao Chen 8e74360d52 Python: Add Microsoft OpenTelemetry Distro sample (#5632)
* Add Microsoft OpenTelemetry Distro sample

* Verify and add README

* Add dependency header

* Add maf dependency in PEP 723 block
2026-07-13 01:51:15 +00:00
ByteWise 7f4cc296fd Python: preserve tool span context for parallel calls (#6512)
* Python: preserve tool span context for parallel calls

* Python: address parallel tool span review feedback

* Python: fix parallel tool span test checks

---------

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-07-13 00:07:53 +00:00
Benke Qu f3057ef20c Python: fix: clear service_session_id in _agent_wrapper when propagate_session=True (#5875)
* fix: clear service_session_id in _agent_wrapper when propagate_session=True

When propagate_session=True, the child agent inherits the parent's
service_session_id. After the parent's first LLM call, MAF auto-populates
this from the Responses API conversation_id. The child sends it as
previous_response_id which the server rejects because the parent's
tool_call is still pending (400 error).

This fix saves and clears service_session_id before calling the child
agent and restores it in a finally block, preserving session.state
sharing while isolating the server-side conversation pointer.

Fixes #5874

* refactor: use child session copy instead of in-place mutation

Address Copilot review comments:
- Create a child AgentSession with shared state dict but isolated
  service_session_id, avoiding race conditions under concurrent
  asyncio.gather tool invocations.
- Update tests to verify child gets a separate session object and
  that child-set service_session_id does not leak to parent.

* fix: update test_chat_agent_as_tool_propagate_session_true for child session isolation

The existing test asserted captured_session is parent_session, but since
we now create a separate child AgentSession (to avoid racing under concurrent
asyncio.gather), the child is a different object. Updated assertions to verify:
- child is NOT the parent object (isolation)
- child shares the same session_id and state dict (by reference)
- child's service_session_id is None (isolated)

* fix: add type narrowing asserts for captured_session

Add 'assert captured_session is not None' before attribute access to
satisfy mypy/pyright type checking on Optional values.

* Python: Fix test typing checks

---------

Co-authored-by: Benke Qu <bequ@microsoft.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
2026-07-12 23:37:53 +00:00
Eduard van Valkenburg 68136ee081 Python: Clean up dependency groups and compatibility (#7046)
* Python: Clean up dependency management

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

Copilot-Session: 2f7b1c89-f3ff-418d-ab4e-4f014fda308f

* Python: Harden Mistral SDK import fallback

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

Copilot-Session: 2f7b1c89-f3ff-418d-ab4e-4f014fda308f
2026-07-10 22:40:42 +00:00
Peter Ibekwe 875031ff56 Fix broken sample 2026-07-10 11:26:05 -07:00
Evan Mattson 87af313119 Python: [BREAKING]: Emit TOOL_CALL events for workflow participant tool calls in AG-UI (#7039)
* Python: emit participant tool calls in AG-UI workflows

Decisions:
- Pass function call, function result, and approval request content from streaming agent updates regardless of role.
- Preserve the assistant-role gate for text and reuse the shared AG-UI content emitters without dual custom-event emission.

Files changed:
- packages/ag-ui/agent_framework_ag_ui/_workflow_run.py
- packages/ag-ui/tests/ag_ui/test_workflow_run.py

Verification:
- uv run poe test -P ag-ui
- uv run poe pyright -P ag-ui
- uv run poe test-typing -P ag-ui
- uv run poe syntax -P ag-ui -C

Notes:
- Existing workflow golden scenarios do not exercise participant tool calls, so no snapshot changed.
- No blockers.

* Python: guard participant tool call duplication

Decisions:
- Assert the workflow stream emits one TOOL_CALL_START when a streamed call is also present in final conversation history.
- Keep production flow unchanged because latest-assistant final-response conversion prevents duplication.

Files changed:
- packages/ag-ui/tests/ag_ui/test_workflow_run.py

Verification:
- uv run pytest packages/ag-ui/tests/ag_ui/test_workflow_run.py -k 'participant_tool_call or repeat_tool_call' -q
- uv run poe test -P ag-ui
- uv run poe pyright -P ag-ui
- uv run poe test-typing -P ag-ui
- uv run poe syntax -P ag-ui -C
- git diff --check

Notes:
- No blockers; no call-id guard was required.

* Python: scope workflow tool content bypass to resumable tool calls

- Exclude approval request content from the role bypass. Workflow approvals
  resume through request_info pending state, so an approval interrupt emitted
  from streamed content would have no pending request to resume against.
- Admit mcp_server_tool_call and mcp_server_tool_result so provider-hosted MCP
  tool calls from workflow participants emit standard tool call events.
- Add unit tests for MCP passthrough, approval exclusion, and mixed
  text-plus-tool content in non-assistant updates.
2026-07-10 16:58:07 +00:00
Evan Mattson 9ac548ad15 Python: keep attachments close (#7038)
* Python: keep attachments close

* Python: close attachment edge cases
2026-07-10 16:57:29 +00:00
Peter Ibekwe 737042fc93 Fix workflow session bug (#7032) 2026-07-10 16:56:57 +00:00
Patrick Woo-Sam e677ccc3b1 .NET: Fix CompactionMessageIndex.IsSummaryMessage (#7042)
* Fix CompactionMessageIndex.IsSummaryMessage

* Add more robust JsonElement value checking and tests cases
2026-07-10 15:03:26 +00:00
Theo van Kraay d9c0c36379 Fix CosmosChatHistoryProvider: omit ttl when MessageTtlSeconds is null (#6992) (#7030) 2026-07-10 13:01:59 +00:00
HaoJun 32a547a1a7 Python: bind AG-UI tool arguments to call IDs (#6342)
Co-authored-by: White-Mouse <15983334+White-Mouse@users.noreply.github.com>
2026-07-10 06:54:53 +00:00
Evan Mattson 7464a59228 Python: Bump Python package versions for 1.11.0 release (#7035)
* Bump Python package versions for 1.11.0 release

Bump the CHANGELOG-selected packages for the 1.11.0 release: core and the root package move to 1.11.0 for the new stable APIs, Foundry and OpenAI receive patch bumps, changed prerelease packages receive the 260709 stamp or next RC counter, and Monty joins the bump set for corrected published dependency metadata. No beta cohort bump was applied. Raise core floors conservatively on every package publishing this cycle and correct dependency floors exposed by lower-bound validation.

Copilot-Session: ee33d338-c1fc-4182-9106-0345ccf26b8e

* Fix Gemini streaming type suppression

Move the targeted Pyright suppression to the SDK contents argument, where the google-genai invariant content-list alias produces the compatibility diagnostic, and remove the now-unnecessary member suppression.

Copilot-Session: ee33d338-c1fc-4182-9106-0345ccf26b8e

* Raise Monty core dependency floor

Align Monty with the conservative release policy by requiring agent-framework-core 1.11.0 or later for the package version published in this cycle.

Copilot-Session: ee33d338-c1fc-4182-9106-0345ccf26b8e
2026-07-10 12:15:26 +09:00
Ethan qu 01ec3b7bcf Fix AG-UI approval thread aliases (#6908)
Co-authored-by: godququ5-code <256881196+godququ5-code@users.noreply.github.com>
2026-07-10 00:51:49 +00:00
westey ce96fd4b72 Python: Integrate message injection into harness agent (#7027)
* Integrate message injection into harness agent and sample console

* Add agents.md update.

* Address PR comments
2026-07-10 00:33:31 +00:00
Evan Mattson 52237b8eff Python: consolidate dependency updates (#7033) 2026-07-10 00:25:30 +00:00
Giles Odigwe e6cc2c09af Python: Fix read_skill_resource instruction dropping .md extension (#7031)
The RESOURCE_INSTRUCTIONS example told the model to use

eferences/FAQ instead of 
eferences/FAQ.md, contradicting the
actual exact-match resource lookup (which lists and matches names
including the extension). This caused read_skill_resource to fail with
'Resource not found'. Align the example with the .NET original.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-10 00:06:09 +00:00
Chinedum Echeta 7440b1c376 feat: enhance tool choice handling for required mode in _prepare_options (#7024) 2026-07-09 22:57:16 +00:00
Peter Ibekwe 3f4ffc6c2c .NET: Fix declarative InvokeAzureAgent failing on non-object JSON agent output (#7002)
* Fix declarative InvokeAzureAgent failing on non-object JSON agent output

* Fix PR comments.
2026-07-09 17:10:51 +00:00
Eduard van Valkenburg d43e52df69 Python: support mem0ai 2.x (#7004)
* Python: support mem0ai 2.x

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

* updated lock

* Address mem0 OSS application scope

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

* Use filters for mem0 platform add

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-09 16:55:51 +00:00
619 changed files with 20236 additions and 5070 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ runs:
using: "composite"
steps:
- name: Set up uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
version-file: "python/pyproject.toml"
enable-cache: true
@@ -46,4 +46,4 @@ runs:
- name: Install the project
shell: bash
run: |
cd python && uv sync --all-packages --all-extras --dev --prerelease=if-necessary-or-explicit
cd python && uv sync --all-packages --all-extras --all-groups --prerelease=if-necessary-or-explicit
+22 -9
View File
@@ -1,25 +1,38 @@
// Copyright (c) Microsoft. All rights reserved.
/**
* Resolve the issue author and check their team membership.
* Resolve the issue or pull request author and check their team membership.
*
* @param {object} opts
* @param {object} opts.github - Octokit REST client from actions/github-script
* @param {object} opts.context - GitHub Actions context
* @param {object} opts.core - GitHub Actions core toolkit
* @param {string} opts.teamSlug - Team slug to check membership against
* @param {string|number} opts.issueNumber - Issue number to resolve author for
* @param {string|number} opts.issueNumber - Issue or pull request number to resolve author for
* @returns {Promise<{author: string|null, isTeamMember: boolean}>}
*/
async function checkTeamMembership({ github, context, core, teamSlug, issueNumber }) {
let author = context.payload.issue?.user?.login;
let author =
context.payload.issue?.user?.login ??
context.payload.pull_request?.user?.login;
if (!author) {
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: Number(issueNumber),
});
author = issue.user?.login;
const number = Number(issueNumber);
if (context.payload.pull_request) {
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: number,
});
author = pr.user?.login;
} else {
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: number,
});
author = issue.user?.login;
}
}
if (!author) {
@@ -0,0 +1,170 @@
// Copyright (c) Microsoft. All rights reserved.
const DECISIVE_REVIEW_STATES = new Set(['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED']);
const SHA_PATTERN = /^[0-9a-f]{40}$/;
const BRANCH_PATTERN = /^[a-zA-Z0-9_./-]+$/;
function assertValidSha(sha, description) {
if (!SHA_PATTERN.test(sha)) {
throw new Error(`GitHub returned an invalid ${description} SHA.`);
}
}
function hasWritePermission(permissionData) {
return permissionData.user?.permissions?.push === true
|| ['admin', 'maintain', 'write'].includes(permissionData.permission);
}
function latestDecisiveReviews(reviews) {
const latestByReviewer = new Map();
const sortedReviews = [...reviews].sort((left, right) => {
const submittedComparison = (left.submitted_at || '').localeCompare(right.submitted_at || '');
return submittedComparison || Number(left.id) - Number(right.id);
});
for (const review of sortedReviews) {
const state = review.state?.toUpperCase();
const reviewer = review.user?.login?.toLowerCase();
if (reviewer && DECISIVE_REVIEW_STATES.has(state)) {
latestByReviewer.set(reviewer, review);
}
}
return latestByReviewer;
}
async function resolvePullRequest({ github, context, core, prNumber, requiredApprovals }) {
if (!/^[0-9]+$/.test(prNumber)) {
throw new Error('Invalid PR number. Only numeric values are allowed.');
}
const pullNumber = Number(prNumber);
const { data: pullRequest } = await github.rest.pulls.get({
...context.repo,
pull_number: pullNumber,
});
if (pullRequest.state !== 'open') {
throw new Error(`PR #${pullNumber} is not open (state: ${pullRequest.state}).`);
}
const headSha = pullRequest.head.sha;
const baseSha = pullRequest.base.sha;
assertValidSha(headSha, 'PR head');
assertValidSha(baseSha, 'PR base');
const reviews = await github.paginate(github.rest.pulls.listReviews, {
...context.repo,
pull_number: pullNumber,
per_page: 100,
});
const latestReviews = latestDecisiveReviews(reviews);
const author = pullRequest.user?.login?.toLowerCase();
const approvalCandidates = [...latestReviews.entries()]
.filter(([, review]) => review.state.toUpperCase() === 'APPROVED')
.filter(([, review]) => review.commit_id === headSha)
.filter(([reviewer]) => reviewer !== author);
const approvedMaintainers = [];
for (const [reviewer] of approvalCandidates) {
const { data: permissionData } = await github.rest.repos.getCollaboratorPermissionLevel({
...context.repo,
username: reviewer,
});
if (hasWritePermission(permissionData)) {
approvedMaintainers.push(reviewer);
} else {
core.info(`Ignoring approval from ${reviewer}: reviewer does not have write permission.`);
}
}
if (approvedMaintainers.length < requiredApprovals) {
throw new Error(
`PR #${pullNumber} head ${headSha} requires ${requiredApprovals} approvals from unique `
+ `write-capable maintainers; found ${approvedMaintainers.length}.`,
);
}
core.info(
`PR #${pullNumber} head ${headSha} approved by: ${approvedMaintainers.join(', ')}.`,
);
return {
baseRef: baseSha,
checkoutRef: headSha,
description: `PR #${pullNumber}`,
};
}
async function resolveBranch({ github, context, core, branch }) {
if (!BRANCH_PATTERN.test(branch)) {
throw new Error(
'Invalid branch name. Only alphanumeric characters, hyphens, underscores, dots, and slashes '
+ 'are allowed.',
);
}
const [{ data: repository }, { data: targetBranch }] = await Promise.all([
github.rest.repos.get(context.repo),
github.rest.repos.getBranch({ ...context.repo, branch }),
]);
const { data: baseBranch } = await github.rest.repos.getBranch({
...context.repo,
branch: repository.default_branch,
});
const checkoutRef = targetBranch.commit.sha;
const baseRef = baseBranch.commit.sha;
assertValidSha(checkoutRef, 'branch head');
assertValidSha(baseRef, 'default branch');
core.info(`Branch ${branch} resolved to immutable commit ${checkoutRef}.`);
return {
baseRef,
checkoutRef,
description: `branch ${branch}`,
};
}
/**
* Resolve a manually requested integration-test target to an immutable commit.
*
* Pull requests must have fresh approvals from two unique write-capable
* maintainers for the exact head commit. Branches are limited to branches in
* the base repository and are pinned to their current commit.
*/
async function resolveIntegrationTestTarget({
github,
context,
core,
prNumber = '',
branch = '',
requiredApprovals = 2,
}) {
const normalizedPrNumber = prNumber.trim();
const normalizedBranch = branch.trim();
if (normalizedPrNumber && normalizedBranch) {
throw new Error('Please provide either a PR number or a branch name, not both.');
}
if (!normalizedPrNumber && !normalizedBranch) {
throw new Error('Please provide either a PR number or a branch name.');
}
if (normalizedPrNumber) {
return resolvePullRequest({
github,
context,
core,
prNumber: normalizedPrNumber,
requiredApprovals,
});
}
return resolveBranch({
github,
context,
core,
branch: normalizedBranch,
});
}
module.exports = resolveIntegrationTestTarget;
+51 -2
View File
@@ -16,7 +16,12 @@ const checkTeamMembership = require('../scripts/check_team_membership.js');
// Helpers
// ---------------------------------------------------------------------------
function createMocks({ payloadIssue = undefined, apiUser = 'api-user', teamState = 'active' } = {}) {
function createMocks({
payloadIssue = undefined,
payloadPullRequest = undefined,
apiUser = 'api-user',
teamState = 'active',
} = {}) {
const core = {
_infoMessages: [],
_failedMessages: [],
@@ -24,8 +29,16 @@ function createMocks({ payloadIssue = undefined, apiUser = 'api-user', teamState
setFailed(msg) { this._failedMessages.push(msg); },
};
const payload = {};
if (payloadIssue !== undefined) {
payload.issue = payloadIssue;
}
if (payloadPullRequest !== undefined) {
payload.pull_request = payloadPullRequest;
}
const context = {
payload: { issue: payloadIssue },
payload,
repo: { owner: 'test-org', repo: 'test-repo' },
};
@@ -36,6 +49,11 @@ function createMocks({ payloadIssue = undefined, apiUser = 'api-user', teamState
data: { user: apiUser ? { login: apiUser } : null },
}),
},
pulls: {
get: async () => ({
data: { user: apiUser ? { login: apiUser } : null },
}),
},
teams: {
getByName: async () => ({}),
getMembershipForUserInOrg: async () => ({
@@ -64,6 +82,37 @@ describe('author resolution', () => {
assert.equal(result.author, 'payload-user');
});
it('resolves author from pull_request event payload', async () => {
const { github, context, core } = createMocks({
payloadPullRequest: { user: { login: 'pr-author' } },
});
let issuesGetCalled = false;
github.rest.issues.get = async () => {
issuesGetCalled = true;
return { data: { user: { login: 'api-user' } } };
};
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
assert.equal(result.author, 'pr-author');
assert.equal(issuesGetCalled, false);
});
it('resolves author via pulls API when pull_request payload user is null', async () => {
const { github, context, core } = createMocks({
payloadPullRequest: { user: null },
apiUser: 'fetched-pr-author',
});
let pullsGetCalled = false;
github.rest.pulls.get = async () => {
pullsGetCalled = true;
return { data: { user: { login: 'fetched-pr-author' } } };
};
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
assert.equal(result.author, 'fetched-pr-author');
assert.equal(pullsGetCalled, true);
});
it('resolves author via API when payload issue is absent', async () => {
const { github, context, core } = createMocks({ apiUser: 'api-user' });
const result = await checkTeamMembership({ github, context, core, ...BASE_OPTS });
@@ -0,0 +1,212 @@
// Copyright (c) Microsoft. All rights reserved.
/**
* Tests for resolve_integration_test_target.js.
*
* Run with: node --test .github/tests/test_resolve_integration_test_target.js
*/
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const resolveIntegrationTestTarget = require('../scripts/resolve_integration_test_target.js');
const HEAD_SHA = 'a'.repeat(40);
const BASE_SHA = 'b'.repeat(40);
function review({
id,
login,
state = 'APPROVED',
commitId = HEAD_SHA,
submittedAt = `2026-07-13T00:00:${String(id).padStart(2, '0')}Z`,
}) {
return {
id,
state,
commit_id: commitId,
submitted_at: submittedAt,
user: { login },
};
}
function createMocks({
pullState = 'open',
pullAuthor = 'contributor',
reviews = [],
permissions = {},
} = {}) {
const core = {
infoMessages: [],
info(message) {
this.infoMessages.push(message);
},
};
const context = {
repo: { owner: 'microsoft', repo: 'agent-framework' },
};
const github = {
paginate: async () => reviews,
rest: {
pulls: {
get: async () => ({
data: {
state: pullState,
user: { login: pullAuthor },
head: { sha: HEAD_SHA },
base: { sha: BASE_SHA },
},
}),
listReviews: async () => {},
},
repos: {
get: async () => ({ data: { default_branch: 'main' } }),
getBranch: async ({ branch }) => ({
data: { commit: { sha: branch === 'main' ? BASE_SHA : HEAD_SHA } },
}),
getCollaboratorPermissionLevel: async ({ username }) => ({
data: permissions[username] || {
permission: 'read',
user: { permissions: { push: false } },
},
}),
},
},
};
return { core, context, github };
}
const WRITE_PERMISSION = {
permission: 'write',
user: { permissions: { push: true } },
};
describe('input validation', () => {
it('rejects missing and conflicting targets', async () => {
const mocks = createMocks();
await assert.rejects(
() => resolveIntegrationTestTarget(mocks),
/provide either a PR number or a branch name/,
);
await assert.rejects(
() => resolveIntegrationTestTarget({ ...mocks, prNumber: '1', branch: 'feature' }),
/not both/,
);
});
it('rejects invalid PR numbers and branch names', async () => {
const mocks = createMocks();
await assert.rejects(
() => resolveIntegrationTestTarget({ ...mocks, prNumber: '1;echo' }),
/Invalid PR number/,
);
await assert.rejects(
() => resolveIntegrationTestTarget({ ...mocks, branch: 'feature branch' }),
/Invalid branch name/,
);
});
});
describe('pull request resolution', () => {
it('pins an open PR with two fresh write-capable approvals', async () => {
const mocks = createMocks({
reviews: [
review({ id: 1, login: 'maintainer-one' }),
review({ id: 2, login: 'maintainer-two' }),
],
permissions: {
'maintainer-one': WRITE_PERMISSION,
'maintainer-two': WRITE_PERMISSION,
},
});
const result = await resolveIntegrationTestTarget({ ...mocks, prNumber: '123' });
assert.deepEqual(result, {
baseRef: BASE_SHA,
checkoutRef: HEAD_SHA,
description: 'PR #123',
});
});
it('rejects closed PRs', async () => {
const mocks = createMocks({ pullState: 'closed' });
await assert.rejects(
() => resolveIntegrationTestTarget({ ...mocks, prNumber: '123' }),
/is not open/,
);
});
it('ignores stale, self, and read-only approvals', async () => {
const mocks = createMocks({
reviews: [
review({ id: 1, login: 'stale', commitId: 'c'.repeat(40) }),
review({ id: 2, login: 'contributor' }),
review({ id: 3, login: 'reader' }),
review({ id: 4, login: 'maintainer' }),
],
permissions: {
contributor: WRITE_PERMISSION,
reader: { permission: 'read', user: { permissions: { push: false } } },
maintainer: WRITE_PERMISSION,
},
});
await assert.rejects(
() => resolveIntegrationTestTarget({ ...mocks, prNumber: '123' }),
/found 1/,
);
});
it('uses each reviewer latest decisive review and ignores later comments', async () => {
const mocks = createMocks({
reviews: [
review({ id: 1, login: 'changes-requested' }),
review({ id: 2, login: 'changes-requested', state: 'CHANGES_REQUESTED' }),
review({ id: 3, login: 'maintainer-one' }),
review({ id: 4, login: 'maintainer-one', state: 'COMMENTED' }),
review({ id: 5, login: 'maintainer-two' }),
],
permissions: {
'changes-requested': WRITE_PERMISSION,
'maintainer-one': WRITE_PERMISSION,
'maintainer-two': WRITE_PERMISSION,
},
});
const result = await resolveIntegrationTestTarget({ ...mocks, prNumber: '123' });
assert.equal(result.checkoutRef, HEAD_SHA);
});
it('does not count a dismissed approval', async () => {
const mocks = createMocks({
reviews: [
review({ id: 1, login: 'dismissed', state: 'DISMISSED' }),
review({ id: 2, login: 'maintainer' }),
],
permissions: {
dismissed: WRITE_PERMISSION,
maintainer: WRITE_PERMISSION,
},
});
await assert.rejects(
() => resolveIntegrationTestTarget({ ...mocks, prNumber: '123' }),
/found 1/,
);
});
});
describe('branch resolution', () => {
it('pins base-repository branches and their comparison base to SHAs', async () => {
const mocks = createMocks();
const result = await resolveIntegrationTestTarget({ ...mocks, branch: 'feature/test' });
assert.deepEqual(result, {
baseRef: BASE_SHA,
checkoutRef: HEAD_SHA,
description: 'branch feature/test',
});
});
});
@@ -23,16 +23,16 @@ For each project that needs to be migrated, you need to do the following:
- Identify the specific Semantic Kernel agent types being used:
- `ChatCompletionAgent``ChatClientAgent`
- `OpenAIAssistantAgent``assistantsClient.CreateAIAgent()` (via OpenAI Assistants client extension)
- `AzureAIAgent``persistentAgentsClient.CreateAIAgent()` (via Azure AI Foundry client extension)
- `AzureAIAgent``persistentAgentsClient.CreateAIAgent()` (via Microsoft Foundry client extension)
- `OpenAIResponseAgent``responsesClient.CreateAIAgent()` (via OpenAI Responses client extension)
- `A2AAgent``AIAgent` (via A2A card resolver)
- `BedrockAgent` → Custom implementation required (not supported)
- Determine if agents are being created new or retrieved from hosted services:
- **New agents**: Use `CreateAIAgent()` methods
- **Existing hosted agents**: Use `GetAIAgent(agentId)` methods for OpenAI Assistants and Azure AI Foundry
- **Existing hosted agents**: Use `GetAIAgent(agentId)` methods for OpenAI Assistants and Microsoft Foundry
</agent_type_identification>
- Determine the AI provider being used (OpenAI, Azure OpenAI, Azure AI Foundry, etc.)
- Determine the AI provider being used (OpenAI, Azure OpenAI, Microsoft Foundry, etc.)
- Analyze tool/function registration patterns
- Review thread management and invocation patterns
@@ -90,7 +90,7 @@ below in wrong order or skip any of them):
you generate report when migration complete. Report should contain:
- all project dependencies changes (mention what was changed, added or removed, including provider-specific packages)
- all code files that were changed (mention what was changed in the file, if it was not changed, just mention that the file was not changed)
- provider-specific migration patterns used (OpenAI, Azure OpenAI, Azure AI Foundry, A2A, ONNX, etc.)
- provider-specific migration patterns used (OpenAI, Azure OpenAI, Microsoft Foundry, A2A, ONNX, etc.)
- all cases where you could not convert the code because of unsupported features and you were unable to find a workaround
- unsupported providers that require custom implementation (Bedrock, CopilotStudio)
- breaking glass pattern migrations (InnerContent → RawRepresentation) and any CodeInterpreter or advanced tool usage
@@ -223,7 +223,7 @@ using Microsoft.Agents.AI;
// Provider-specific namespaces (add only if needed):
using OpenAI; // For OpenAI provider
using Azure.AI.OpenAI; // For Azure OpenAI provider
using Azure.AI.Agents.Persistent; // For Azure AI Foundry provider
using Azure.AI.Agents.Persistent; // For Microsoft Foundry provider
using Azure.Identity; // For Azure authentication
```
</configuration_changes>
@@ -499,7 +499,7 @@ For every thread created if there's intent to cleanup, the caller should track a
var assistantClient = new OpenAIClient(apiKey).GetAssistantClient();
await assistantClient.DeleteThreadAsync(thread.ConversationId);
// For Azure AI Foundry (when cleanup is needed):
// For Microsoft Foundry (when cleanup is needed):
var persistentClient = new PersistentAgentsClient(endpoint, credential);
await persistentClient.Threads.DeleteThreadAsync(thread.ConversationId);
@@ -514,7 +514,7 @@ await persistentClient.Threads.DeleteThreadAsync(thread.ConversationId);
1. Remove `thread.DeleteAsync()` calls
2. Use provider-specific client for cleanup when required
3. Access thread ID via `thread.ConversationId` property
4. Only implement cleanup for providers that require it (Assistants, Azure AI Foundry)
4. Only implement cleanup for providers that require it (Assistants, Microsoft Foundry)
</api_changes>
### Provider-Specific Creation Patterns
@@ -550,13 +550,13 @@ AIAgent agent = new AzureOpenAIClient(endpoint, credential)
.CreateAIAgent(instructions: instructions);
```
**Azure AI Foundry (New):**
**Microsoft Foundry (New):**
```csharp
AIAgent agent = new PersistentAgentsClient(endpoint, credential)
.CreateAIAgent(model: deploymentName, instructions: instructions);
```
**Azure AI Foundry (Existing):**
**Microsoft Foundry (Existing):**
```csharp
AIAgent agent = await new PersistentAgentsClient(endpoint, credential)
.GetAIAgentAsync(agentId);
@@ -1079,7 +1079,7 @@ AgentThread thread = agent.GetNewThread();
```
</api_changes>
### 4. Azure AI Foundry (AzureAIAgent) Migration
### 4. Microsoft Foundry (AzureAIAgent) Migration
<configuration_changes>
**Remove Semantic Kernel Packages:**
+3 -3
View File
@@ -38,7 +38,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -51,7 +51,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -64,6 +64,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -140,7 +140,7 @@ jobs:
python-version: "3.13"
- name: Set up uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
version: "0.11.x"
enable-cache: true
+6 -5
View File
@@ -42,7 +42,7 @@ jobs:
coreChanged: ${{ steps.filter.outputs.core }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: filter
with:
filters: |
@@ -163,6 +163,7 @@ jobs:
# Change to project directory to ensure local nuget.config is used
pushd consoleapp
dotnet add packcheck.csproj package Microsoft.Agents.AI --prerelease
dotnet add packcheck.csproj package Microsoft.Agents.AI.LocalCodeAct --prerelease
dotnet build -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} packcheck.csproj
# Clean up
@@ -312,7 +313,7 @@ jobs:
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
# Azure AI Foundry
# Microsoft Foundry
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_MODEL_DEPLOYMENT_NAME }}
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
@@ -528,7 +529,7 @@ jobs:
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
# Azure AI Foundry
# Microsoft Foundry
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_MODEL_DEPLOYMENT_NAME }}
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
@@ -615,7 +616,7 @@ jobs:
pattern: dotnet-test-results-*
path: dotnet-test-results/
- name: Restore report history cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: python/dotnet-integration-report-history.json
key: dotnet-integration-report-history-${{ github.run_id }}
@@ -632,7 +633,7 @@ jobs:
run: cat dotnet-integration-test-report.md >> $GITHUB_STEP_SUMMARY
- name: Save report history cache
if: always()
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: python/dotnet-integration-report-history.json
key: dotnet-integration-report-history-${{ github.run_id }}
+17 -2
View File
@@ -9,16 +9,31 @@ on:
workflow_call:
inputs:
checkout-ref:
description: "Git ref to checkout (e.g., refs/pull/123/head)"
description: "Immutable commit SHA to check out"
required: true
type: string
secrets:
AZURE_CLIENT_ID:
required: true
AZURE_TENANT_ID:
required: true
AZURE_SUBSCRIPTION_ID:
required: true
AZUREAI__ENDPOINT:
required: true
COPILOT_GITHUB_TOKEN:
required: true
OPENAI__APIKEY:
required: true
permissions:
contents: read
id-token: write
jobs:
dotnet-integration-tests:
permissions:
contents: read
id-token: write
strategy:
fail-fast: false
matrix:
+1 -1
View File
@@ -105,7 +105,7 @@ jobs:
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
# Azure AI Foundry
# Microsoft Foundry
AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.AZURE_AI_MODEL_DEPLOYMENT_NAME }}
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
+53 -52
View File
@@ -3,7 +3,7 @@
# Go to Actions → "Integration Tests (Manual)" → Run workflow → enter a PR number or branch name.
#
# It calls dedicated integration-only workflows (dotnet-integration-tests and python-integration-tests),
# passing a ref so they check out and test the correct code.
# passing an immutable commit SHA so they check out and test the approved code.
# Changed paths are detected here so only the relevant test suites run.
#
@@ -26,7 +26,6 @@ on:
permissions:
contents: read
pull-requests: read
id-token: write
concurrency:
group: integration-tests-manual-${{ github.event.inputs.pr-number || github.event.inputs.branch }}
@@ -38,67 +37,50 @@ jobs:
runs-on: ubuntu-latest
outputs:
checkout-ref: ${{ steps.resolve.outputs.checkout-ref }}
base-ref: ${{ steps.resolve.outputs.base-ref }}
dotnet-changes: ${{ steps.detect-changes.outputs.dotnet }}
python-changes: ${{ steps.detect-changes.outputs.python }}
steps:
- name: Resolve checkout ref
- name: Check out trusted workflow helpers
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.sha }}
persist-credentials: false
sparse-checkout: .github/scripts
- name: Resolve and authorize checkout ref
id: resolve
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const resolveIntegrationTestTarget = require(
'./.github/scripts/resolve_integration_test_target.js'
);
const target = await resolveIntegrationTestTarget({
github,
context,
core,
prNumber: process.env.PR_NUMBER,
branch: process.env.BRANCH,
});
core.setOutput('checkout-ref', target.checkoutRef);
core.setOutput('base-ref', target.baseRef);
core.info(`Running integration tests for ${target.description} at ${target.checkoutRef}.`);
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.inputs.pr-number }}
BRANCH: ${{ github.event.inputs.branch }}
REPO: ${{ github.repository }}
run: |
if [ -n "$PR_NUMBER" ] && [ -n "$BRANCH" ]; then
echo "::error::Please provide either a PR number or a branch name, not both."
exit 1
fi
if [ -z "$PR_NUMBER" ] && [ -z "$BRANCH" ]; then
echo "::error::Please provide either a PR number or a branch name."
exit 1
fi
if [ -n "$PR_NUMBER" ]; then
if ! echo "$PR_NUMBER" | grep -Eq '^[0-9]+$'; then
echo "::error::Invalid PR number. Only numeric values are allowed."
exit 1
fi
PR_DATA=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json state)
PR_STATE=$(echo "$PR_DATA" | jq -r '.state')
if [ "$PR_STATE" != "OPEN" ]; then
echo "::error::PR #$PR_NUMBER is not open (state: $PR_STATE)"
exit 1
fi
echo "checkout-ref=refs/pull/$PR_NUMBER/head" >> "$GITHUB_OUTPUT"
echo "Running integration tests for PR #$PR_NUMBER"
else
if ! echo "$BRANCH" | grep -Eq '^[a-zA-Z0-9_./-]+$'; then
echo "::error::Invalid branch name. Only alphanumeric characters, hyphens, underscores, dots, and slashes are allowed."
exit 1
fi
echo "checkout-ref=$BRANCH" >> "$GITHUB_OUTPUT"
echo "Running integration tests for branch $BRANCH"
fi
- name: Detect changed paths
id: detect-changes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.inputs.pr-number }}
BRANCH: ${{ github.event.inputs.branch }}
BASE_REF: ${{ steps.resolve.outputs.base-ref }}
CHECKOUT_REF: ${{ steps.resolve.outputs.checkout-ref }}
REPO: ${{ github.repository }}
run: |
if [ -n "$PR_NUMBER" ]; then
CHANGED_FILES=$(gh pr diff "$PR_NUMBER" --repo "$REPO" --name-only)
else
# For branches, compare against main using the GitHub API
CHANGED_FILES=$(gh api "repos/$REPO/compare/main...$BRANCH" --jq '.files[].filename')
fi
CHANGED_FILES=$(gh api "repos/$REPO/compare/$BASE_REF...$CHECKOUT_REF" \
--jq '.files[].filename')
DOTNET_CHANGES=false
PYTHON_CHANGES=false
@@ -113,22 +95,41 @@ jobs:
echo "dotnet=$DOTNET_CHANGES" >> "$GITHUB_OUTPUT"
echo "python=$PYTHON_CHANGES" >> "$GITHUB_OUTPUT"
echo "Detected changes dotnet: $DOTNET_CHANGES, python: $PYTHON_CHANGES"
echo "Detected changes; dotnet: $DOTNET_CHANGES, python: $PYTHON_CHANGES"
dotnet-integration-tests:
name: .NET Integration Tests
needs: resolve-ref
if: needs.resolve-ref.outputs.dotnet-changes == 'true'
permissions:
contents: read
id-token: write
uses: ./.github/workflows/dotnet-integration-tests.yml
with:
checkout-ref: ${{ needs.resolve-ref.outputs.checkout-ref }}
secrets: inherit
secrets:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZUREAI__ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
OPENAI__APIKEY: ${{ secrets.OPENAI__APIKEY }}
python-integration-tests:
name: Python Integration Tests
needs: resolve-ref
if: needs.resolve-ref.outputs.python-changes == 'true'
permissions:
contents: read
id-token: write
uses: ./.github/workflows/python-integration-tests.yml
with:
checkout-ref: ${{ needs.resolve-ref.outputs.checkout-ref }}
secrets: inherit
secrets:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
FOUNDRY_MODELS_API_KEY: ${{ secrets.FOUNDRY_MODELS_API_KEY }}
OPENAI__APIKEY: ${{ secrets.OPENAI__APIKEY }}
+27 -7
View File
@@ -3,6 +3,12 @@ name: Issue Triage
on:
issues:
types: [opened, typed]
workflow_dispatch:
inputs:
issue_number:
description: Issue number to triage
required: true
type: string
permissions:
contents: read
@@ -12,7 +18,8 @@ permissions:
concurrency:
group: >-
issue-triage-${{ github.repository }}-${{
github.event.issue.type.name == 'Bug' && github.event.issue.number
github.event_name == 'workflow_dispatch' && inputs.issue_number
|| github.event.issue.type.name == 'Bug' && github.event.issue.number
|| github.run_id
}}
cancel-in-progress: true
@@ -26,7 +33,11 @@ env:
jobs:
team_check:
runs-on: ubuntu-latest
if: ${{ github.event.issue.type.name == 'Bug' }}
if: >-
${{
github.event_name == 'workflow_dispatch'
|| github.event.issue.type.name == 'Bug'
}}
outputs:
is_team_member: ${{ steps.check.outputs.is_team_member }}
issue_number: ${{ steps.issue.outputs.issue_number }}
@@ -36,14 +47,18 @@ jobs:
id: issue
shell: bash
env:
ISSUE_NUMBER_EVENT: ${{ github.event.issue.number }}
ISSUE_NUMBER: >-
${{
github.event_name == 'workflow_dispatch' && inputs.issue_number
|| github.event.issue.number
}}
run: |
set -euo pipefail
issue_number="${ISSUE_NUMBER_EVENT}"
issue_number="${ISSUE_NUMBER}"
if [[ ! "$issue_number" =~ ^[1-9][0-9]*$ ]]; then
echo "Could not determine issue number from event payload." >&2
echo "Could not determine issue number from event payload or manual input." >&2
exit 1
fi
@@ -58,6 +73,7 @@ jobs:
persist-credentials: false
- name: Check issue author team membership
if: ${{ github.event_name != 'workflow_dispatch' }}
id: check
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
@@ -84,7 +100,11 @@ jobs:
triage:
runs-on: ubuntu-latest
needs: team_check
if: ${{ needs.team_check.outputs.is_team_member == 'false' }}
if: >-
${{
github.event_name == 'workflow_dispatch'
|| needs.team_check.outputs.is_team_member == 'false'
}}
environment: integration
timeout-minutes: 60
@@ -114,7 +134,7 @@ jobs:
python-version: "3.13"
- name: Set up uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
version: "0.11.x"
enable-cache: true
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
version-file: "python/pyproject.toml"
enable-cache: true
+30 -4
View File
@@ -13,13 +13,27 @@ on:
workflow_call:
inputs:
checkout-ref:
description: "Git ref to checkout (e.g., refs/pull/123/head)"
description: "Immutable commit SHA to check out"
required: true
type: string
secrets:
ANTHROPIC_API_KEY:
required: true
AZURE_CLIENT_ID:
required: true
AZURE_TENANT_ID:
required: true
AZURE_SUBSCRIPTION_ID:
required: true
COPILOT_GITHUB_TOKEN:
required: true
FOUNDRY_MODELS_API_KEY:
required: false
OPENAI__APIKEY:
required: true
permissions:
contents: read
id-token: write
env:
UV_CACHE_DIR: /tmp/.uv-cache
@@ -99,6 +113,9 @@ jobs:
# Azure OpenAI integration tests
python-tests-azure-openai:
name: Python Integration Tests - Azure OpenAI
permissions:
contents: read
id-token: write
runs-on: ubuntu-latest
environment: integration
timeout-minutes: 60
@@ -260,6 +277,9 @@ jobs:
# Azure Functions + Durable Task integration tests
python-tests-functions:
name: Python Integration Tests - Functions
permissions:
contents: read
id-token: write
runs-on: ubuntu-latest
environment: integration
timeout-minutes: 60
@@ -324,6 +344,9 @@ jobs:
# Foundry integration tests
python-tests-foundry:
name: Python Integration Tests - Foundry
permissions:
contents: read
id-token: write
runs-on: ubuntu-latest
environment: integration
timeout-minutes: 60
@@ -378,6 +401,9 @@ jobs:
# Foundry Hosting integration tests
python-tests-foundry-hosting:
name: Python Integration Tests - Foundry Hosting
permissions:
contents: read
id-token: write
runs-on: ubuntu-latest
environment: integration
timeout-minutes: 60
@@ -551,7 +577,7 @@ jobs:
pattern: test-results-*
path: test-results/
- name: Restore report history cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: python/integration-report-history.json
key: integration-report-history-integration-${{ github.run_id }}
@@ -568,7 +594,7 @@ jobs:
run: cat integration-test-report.md >> $GITHUB_STEP_SUMMARY
- name: Save report history cache
if: always()
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: python/integration-report-history.json
key: integration-report-history-integration-${{ github.run_id }}
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
pythonChanges: ${{ steps.filter.outputs.python}}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: filter
with:
filters: |
@@ -71,7 +71,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
os: ${{ runner.os }}
exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot' || '' }}
exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot agent-framework-azure-cosmos-memory' || '' }}
env:
# Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache
+3 -3
View File
@@ -43,7 +43,7 @@ jobs:
githubCopilotChanged: ${{ steps.filter.outputs.github_copilot }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: filter
with:
filters: |
@@ -748,7 +748,7 @@ jobs:
pattern: test-results-*
path: test-results/
- name: Restore report history cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: python/integration-report-history.json
key: integration-report-history-merge-${{ github.run_id }}
@@ -765,7 +765,7 @@ jobs:
run: cat integration-test-report.md >> $GITHUB_STEP_SUMMARY
- name: Save report history cache
if: always()
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: python/integration-report-history.json
key: integration-report-history-merge-${{ github.run_id }}
+1 -1
View File
@@ -56,7 +56,7 @@ jobs:
- name: Build the package
run: uv run poe --directory packages/${{ env.PACKAGE }} build
- name: Release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
with:
files: |
python/dist/*
@@ -701,7 +701,7 @@ jobs:
- name: Restore validation history
id: cache-restore
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: validation-history/
key: validation-history-${{ github.run_id }}
@@ -719,7 +719,7 @@ jobs:
run: cat trend-report.md >> "$GITHUB_STEP_SUMMARY"
- name: Save validation history
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: validation-history/
key: validation-history-${{ github.run_id }}
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
os: ${{ runner.os }}
exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot' || '' }}
exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot agent-framework-azure-cosmos-memory' || '' }}
env:
# Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache
+2 -2
View File
@@ -2,7 +2,7 @@
**What is Microsoft Agent Framework?**
Microsoft Agent Framework is a comprehensive multi-language (C#/.NET and Python) framework for building, orchestrating, and deploying AI agents and multi-agent workflows. The system takes user instructions and conversation inputs and produces intelligent responses through AI agents that can integrate with various LLM providers (OpenAI, Azure OpenAI, Azure AI Foundry). It provides both simple chat agents and complex multi-agent workflows with graph-based orchestration.
Microsoft Agent Framework is a comprehensive multi-language (C#/.NET and Python) framework for building, orchestrating, and deploying AI agents and multi-agent workflows. The system takes user instructions and conversation inputs and produces intelligent responses through AI agents that can integrate with various LLM providers (OpenAI, Azure OpenAI, Microsoft Foundry). It provides both simple chat agents and complex multi-agent workflows with graph-based orchestration.
**What can Microsoft Agent Framework do?**
@@ -12,7 +12,7 @@ The framework offers:
- **Multi-Agent Orchestration**: Group chat, sequential, concurrent, and handoff patterns
- **Graph-based Workflows**: Connect agents and deterministic functions using data flows with streaming, checkpointing, time-travel, and Human-in-the-loop
- **Extensibility Framework**: Extend with native functions, A2A, Model Context Protocol (MCP)
- **LLM Integration**: Support for OpenAI, Azure OpenAI, Azure AI Foundry, and other providers
- **LLM Integration**: Support for OpenAI, Azure OpenAI, Microsoft Foundry, and other providers
- **Runtime Support**: Both in-process and distributed agent execution
**What is/are Microsoft Agent Framework's intended use(s)?**
+2 -2
View File
@@ -2,7 +2,7 @@
# These are optional elements. Feel free to remove any of them.
status: accepted
contact: westey-m
date: 2025-07-10 {YYYY-MM-DD when the decision was last updated}
date: 2025-07-10
deciders: sergeymenshykh, markwallace, rbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub
consulted:
informed:
@@ -139,7 +139,7 @@ Therefore something like `AgentResponse.Text` which also aggregates all `TextCon
#### Option 1.2 Presence of Secondary Content is determined by a runtime parameter
We can allow callers to choose whether to include secondary content in the list of reponse messages.
We can allow callers to choose whether to include secondary content in the list of response messages.
Open Question: Do we allow secondary content to use `TextContent` types?
```csharp
+8 -8
View File
@@ -113,7 +113,7 @@ Implement a hybrid strategy where common tools use generic `AITool`-derived abst
### AI Agent Tool Types Availability
Tool Type | Azure AI Foundry Agent Service | OpenAI Assistant API | OpenAI ChatCompletion API | OpenAI Responses API | Amazon Bedrock Agents | Google | Anthropic | Description
Tool Type | Microsoft Foundry Agent Service | OpenAI Assistant API | OpenAI ChatCompletion API | OpenAI Responses API | Amazon Bedrock Agents | Google | Anthropic | Description
-- | -- | -- | -- | -- | -- | -- | -- | --
Function Calling | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Enables custom, stateless functions to define specific agent behaviors.
Code Interpreter | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | Allows agents to execute code for tasks like data analysis or problem-solving.
@@ -132,7 +132,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
#### Function Calling
<details>
<summary>Azure AI Foundry Agent Service</summary>
<summary>Microsoft Foundry Agent Service</summary>
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/function-calling?pivots=rest">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/function-calling?pivots=rest</a>
Message Request:
@@ -401,7 +401,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
#### Code Interpreter
<details>
<summary>Azure AI Foundry Agent Service</summary>
<summary>Microsoft Foundry Agent Service</summary>
<p>Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/code-interpreter-samples?pivots=rest-api">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/code-interpreter-samples?pivots=rest-api</a></p>
<p>.NET Support: ✅</p>
@@ -709,7 +709,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
#### Search and Retrieval
<details>
<summary>Azure AI Foundry Agent Service</summary>
<summary>Microsoft Foundry Agent Service</summary>
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/file-search-upload-files?pivots=rest">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/file-search-upload-files?pivots=rest</a>
File Search Request:
@@ -1083,7 +1083,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
#### Web Search
<details>
<summary>Azure AI Foundry Agent Service</summary>
<summary>Microsoft Foundry Agent Service</summary>
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/bing-code-samples?pivots=rest">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/bing-code-samples?pivots=rest</a>
Bing Search Message Request:
@@ -1630,7 +1630,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
#### OpenAPI Spec Tool
<details>
<summary>Azure AI Foundry Agent Service</summary>
<summary>Microsoft Foundry Agent Service</summary>
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/openapi-spec-samples?pivots=rest-api">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/openapi-spec-samples?pivots=rest-api</a><br>
Source: <a href="https://learn.microsoft.com/en-us/rest/api/aifoundry/aiagents/run-steps/get-run-step?view=rest-aifoundry-aiagents-v1&tabs=HTTP#runstepopenapitoolcall">https://learn.microsoft.com/en-us/rest/api/aifoundry/aiagents/run-steps/get-run-step?view=rest-aifoundry-aiagents-v1&tabs=HTTP#runstepopenapitoolcall</a>
@@ -1712,7 +1712,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
#### Stateful Functions
<details>
<summary>Azure AI Foundry Agent Service</summary>
<summary>Microsoft Foundry Agent Service</summary>
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/azure-functions-samples?pivots=rest">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/azure-functions-samples?pivots=rest</a>
Message Request:
@@ -1832,7 +1832,7 @@ Image Generation | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | Generates or edits
#### Microsoft Fabric
<details>
<summary>Azure AI Foundry Agent Service</summary>
<summary>Microsoft Foundry Agent Service</summary>
Source: <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/fabric?pivots=rest">https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/fabric?pivots=rest</a>
Message Request:
+2 -2
View File
@@ -2,7 +2,7 @@
# These are optional elements. Feel free to remove any of them.
status: accepted
contact: westey-m
date: 2025-09-12 {YYYY-MM-DD when the decision was last updated}
date: 2025-09-12
deciders: sergeymenshykh, markwallace-microsoft, rogerbarreto, dmytrostruk, westey-m, eavanvalkenburg, stephentoub, peterychang
consulted:
informed:
@@ -25,7 +25,7 @@ See various features that would need to be supported via this type of mechanism,
- Also see [the openai human-in-the-loop guide](https://openai.github.io/openai-agents-js/guides/human-in-the-loop/#approval-requests).
- Also see [the openai MCP guide](https://openai.github.io/openai-agents-js/guides/mcp/#optional-approval-flow).
- Also see [MCP Approval Requests from OpenAI](https://platform.openai.com/docs/guides/tools-remote-mcp#approvals).
- Also see [Azure AI Foundry MCP Approvals](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/model-context-protocol-samples?pivots=rest#submit-your-approval).
- Also see [Microsoft Foundry MCP Approvals](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/tools/model-context-protocol-samples?pivots=rest#submit-your-approval).
- Also see [MCP Elicitation requests](https://modelcontextprotocol.io/specification/draft/client/elicitation)
## Decision Drivers
@@ -57,7 +57,7 @@ This section describes different options for various aspects required to add lon
### 1. Methods for Working with Long-Running Operations
Based on the analysis of existing APIs that support long-running operations (such as OpenAI Responses, Azure AI Foundry Agents, and A2A),
Based on the analysis of existing APIs that support long-running operations (such as OpenAI Responses, Microsoft Foundry Agents, and A2A),
the following operations are used for working with long-running operations:
- Common operations:
- **Start Long-Running Execution**: Initiates a long-running operation and returns its Id.
@@ -757,7 +757,7 @@ Some of them natively support resuming streaming from a specific point in the st
| API | Can Resume Streaming | Model |
|-------------------------|--------------------------------------|------------------------------------------------------------------------------------------------------------|
| OpenAI Responses | Yes | StreamingResponseUpdate.**SequenceNumber** + GetResponseStreamingAsync(responseId, **startingAfter**, ct) |
| Azure AI Foundry Agents | Emulated<sup>2</sup> | RunStep.**Id** + custom pseudo code: client.Runs.GetRunStepsAsync(...).AllStepsAfter(**stepId**) |
| Microsoft Foundry Agents | Emulated<sup>2</sup> | RunStep.**Id** + custom pseudo code: client.Runs.GetRunStepsAsync(...).AllStepsAfter(**stepId**) |
| A2A | Implementation dependent<sup>1</sup> | |
<sup>1</sup> The [A2A specification](https://github.com/a2aproject/A2A/blob/main/docs/topics/streaming-and-async.md#1-streaming-with-server-sent-events-sse)
@@ -765,7 +765,7 @@ allows an A2A agent implementation to decide how to handle streaming resumption:
a task is still active (and the server hasn't sent a final: true event for that phase), the client can attempt to reconnect to the stream using the tasks/resubscribe RPC method.
The server's behavior regarding missed events during the disconnection period (e.g., whether it backfills or only sends new updates) is implementation-dependent._
<sup>2</sup> The Azure AI Foundry Agents API has an API to start a streaming run but does not have an API to resume streaming from a specific point in the stream.
<sup>2</sup> The Microsoft Foundry Agents API has an API to start a streaming run but does not have an API to resume streaming from a specific point in the stream.
However, it has non-streaming APIs to access already started runs, which can be used to emulate streaming resumption by accessing a run and its steps and streaming all the steps after a specific step.
#### Required Changes
@@ -828,7 +828,7 @@ Sequence of updates from OpenAI Responses API to answer the question "What time
| resp_2 | 10 | resp.output_item.done | - | InProgress | |
| resp_2 | 11 | resp.completed | Completed | Completed | |
Sequence of updates from Azure AI Foundry Agents API to answer the question "What time is it?" using a function call:
Sequence of updates from Microsoft Foundry Agents API to answer the question "What time is it?" using a function call:
| Id | SN | UpdateKind | Run.Status | Step.Status | Message.Status | ChatResponseUpdate.Status | Description |
|--------|---------|-------------------|----------------|-------------|-----------------|---------------------------|---------------------------------------------------|
| run_1 | - | RunCreated | Queued | - | - | Queued | |
@@ -852,7 +852,7 @@ Sequence of updates from Azure AI Foundry Agents API to answer the question "Wha
To support long-running operations, the following values need to be returned by the GetResponseAsync and GetStreamingResponseAsync methods:
- `ResponseId` - identifier of the long-running operation or an entity representing it, such as a task.
- `ConversationId` - identifier of the conversation or thread the long-running operation is part of. Some APIs, like Azure AI Foundry Agents, use
- `ConversationId` - identifier of the conversation or thread the long-running operation is part of. Some APIs, like Microsoft Foundry Agents, use
this identifier together with the ResponseId to identify a run.
- `SequenceNumber` - identifier of an update within a stream of updates. This is required to support streaming resumption by the GetStreamingResponseAsync method only.
- `Status` - status of the long-running operation: whether it is queued, running, failed, cancelled, completed, etc.
@@ -1089,7 +1089,7 @@ public class ChatOptions
##### 6.1.5 Continuation Token of a Custom Type
The option is similar the the "6.1.3 Continuation Token of System.ClientModel.ContinuationToken Type" option but suggests using a
The option is similar to the "6.1.3 Continuation Token of System.ClientModel.ContinuationToken Type" option but suggests using a
custom type for the continuation token instead of the `System.ClientModel.ContinuationToken` type.
**Pros**
@@ -1203,7 +1203,7 @@ response = await agent.CancelRunAsync(response.ResponseId, new AgentCancelRunOpt
In case an agent supports either or both cancellation and deletion of long-running operations, it will override the corresponding methods.
Otherwise, it won't override them, and the base implementations will return null by default.
Some agents, for example Azure AI Foundry Agents, require the thread identifier to cancel a run. To accommodate this requirement, the `CancelRunAsync` method
Some agents, for example Microsoft Foundry Agents, require the thread identifier to cancel a run. To accommodate this requirement, the `CancelRunAsync` method
accepts an optional `AgentCancelRunOptions` parameter that allows callers to specify the thread associated with the run they want to cancel.
```csharp
@@ -1574,7 +1574,7 @@ the thread is provided with background operations consistently for all runs.
</details>
<details>
<summary>Azure AI Foundry Agents</summary>
<summary>Microsoft Foundry Agents</summary>
- Create a thread and run the agent against it and wait for it to complete using polling:
```csharp
@@ -34,11 +34,11 @@ Key changes:
1. **New `agent-framework-openai` package** with dependencies on `agent-framework-core`, `openai`, and `packaging` only.
2. **Class renames**: `OpenAIResponsesClient``OpenAIChatClient` (Responses API), `OpenAIChatClient``OpenAIChatCompletionClient` (Chat Completions API). Old names remain as deprecated aliases.
3. **Deprecated classes**: `OpenAIAssistantsClient`, all `AzureOpenAI*Client` classes, `AzureAIClient`, `AzureAIAgentClient`, and `AzureAIProjectAgentProvider` are marked deprecated.
4. **New `FoundryChatClient`** in azure-ai for Azure AI Foundry Responses API access, built on `RawFoundryChatClient(RawOpenAIChatClient)`.
4. **New `FoundryChatClient`** in azure-ai for Microsoft Foundry Responses API access, built on `RawFoundryChatClient(RawOpenAIChatClient)`.
5. **All deprecated `AzureOpenAI*` classes** consolidated into a single file (`_deprecated_azure_openai.py`) in the azure-ai package for clean future deletion.
6. **Core's `agent_framework.openai` and `agent_framework.azure` namespaces** become lazy-loading gateways, preserving backward-compatible import paths while removing hard dependencies.
7. **Unified `model` parameter** replaces `model_id` (OpenAI), `deployment_name` (Azure OpenAI), and `model_deployment_name` (Azure AI) across all client constructors. The term `model` is intentionally generic: it naturally maps to an OpenAI model name *and* to an Azure OpenAI deployment name, making it straightforward to use `OpenAIChatClient` with either OpenAI or Azure OpenAI backends (via `AsyncAzureOpenAI`). Environment variables are similarly unified (e.g., `OPENAI_MODEL` instead of separate `OPENAI_CHAT_MODEL_ID` / `OPENAI_CHAT_COMPLETION_MODEL_ID`).
8. **`FoundryAgent`** replaces the pattern of `Agent(client=AzureAIClient(...))` for connecting to pre-configured agents in Azure AI Foundry (PromptAgents and HostedAgents). The underlying `RawFoundryAgentChatClient` is an implementation detail — most users interact only with `FoundryAgent`. `AzureAIAgentClient` is separately deprecated as it refers to the V1 Agents Service API. See below for design rationale.
8. **`FoundryAgent`** replaces the pattern of `Agent(client=AzureAIClient(...))` for connecting to pre-configured agents in Microsoft Foundry (PromptAgents and HostedAgents). The underlying `RawFoundryAgentChatClient` is an implementation detail — most users interact only with `FoundryAgent`. `AzureAIAgentClient` is separately deprecated as it refers to the V1 Agents Service API. See below for design rationale.
### Foundry Agent Design: `FoundryAgentClient` vs `FoundryAgent`
@@ -7,11 +7,11 @@ consulted: Pratyush Mishra, Shivam Shrivastava, Manni Arora (Centrica eval scena
informed: Agent Framework team, Foundry Evals team
---
# Agent Evaluation Architecture with Azure AI Foundry Integration
# Agent Evaluation Architecture with Microsoft Foundry Integration
## Context and Problem Statement
Azure AI Foundry provides a rich evaluation service for AI agents — built-in evaluators for agent behavior (task adherence, intent resolution), tool usage (tool call accuracy, tool selection), quality (coherence, fluency, relevance), and safety (violence, self-harm, prohibited actions). Results are viewable in the Foundry portal with dashboards and comparison views.
Microsoft Foundry provides a rich evaluation service for AI agents — built-in evaluators for agent behavior (task adherence, intent resolution), tool usage (tool call accuracy, tool selection), quality (coherence, fluency, relevance), and safety (violence, self-harm, prohibited actions). Results are viewable in the Foundry portal with dashboards and comparison views.
However, using Foundry Evals with an agent-framework agent today requires significant manual effort. Developers must:
@@ -445,7 +445,7 @@ These factorings produce different scores for the same conversation. The framewo
### Azure AI: FoundryEvals
`Evaluator` implementation backed by Azure AI Foundry:
`Evaluator` implementation backed by Microsoft Foundry:
```python
class FoundryEvals:
@@ -812,4 +812,4 @@ public sealed class EvalItem
## More Information
- [Foundry Evals documentation](https://learn.microsoft.com/azure/ai-foundry/concepts/evaluation-approach-gen-ai) — Azure AI Foundry evaluation overview
- [Foundry Evals documentation](https://learn.microsoft.com/azure/ai-foundry/concepts/evaluation-approach-gen-ai) — Microsoft Foundry evaluation overview
@@ -9,7 +9,7 @@ deciders: evmattso
## What is the goal of this feature?
Enable Agent Framework users to consume Foundry **toolboxes** — named, versioned bundles of tool definitions stored server-side in an Azure AI Foundry project — directly from `FoundryChatClient`, without dropping to the raw `azure-ai-projects` SDK.
Enable Agent Framework users to consume Foundry **toolboxes** — named, versioned bundles of tool definitions stored server-side in a Microsoft Foundry project — directly from `FoundryChatClient`, without dropping to the raw `azure-ai-projects` SDK.
A user who has configured a toolbox in the Foundry portal (or via the raw SDK) should be able to load it into an agent with a single call:
+9 -3
View File
@@ -181,8 +181,8 @@ The app chooses which helper to call for that route and deployment. For example:
- `responses_session_id(body)` from `agent-framework-hosting-responses`, which can return either a `resp_*` previous
response id or a `conv_*` conversation id when present;
- `telegram_session_id(update)` from `agent-framework-hosting-telegram`, which can choose the chat, user, thread, or
other Telegram-native partitioning logic for that helper;
- `telegram_session_id(update, bot_id=...)` from `agent-framework-hosting-telegram`, which uses the bot and sender for
private chats and the bot and chat for shared group sessions;
- `activity_session_id(activity)`, `discord_session_id(interaction_or_message)`, or
`a2a_session_id(request_context)` from their respective protocol packages;
- `foundry_user_isolation_key()` or `foundry_chat_isolation_key()` from `agent-framework-foundry-hosting`.
@@ -204,7 +204,8 @@ safe to use:
transient execution.
A `SessionStore` stores `session_id -> AgentSession`, but it does not create sessions. `AgentState` resolves the agent
target and creates the session on first use:
target and creates the session on first use. Reads return independent working copies so running from one continuation
point does not mutate the stored snapshot or another simultaneous branch:
For agent targets:
@@ -227,6 +228,11 @@ await state.set_session(response_id, session)
`agent.run(...)` may update the session object (for example, with service continuation state), so the explicit store call
belongs after the run, not before it.
Response ids are immutable continuation points, so simultaneous callers can branch from one `previous_response_id` and
store their completed sessions under different new response ids. A stable `conversation_id` is a mutable head: the app
must explicitly update it after the run and provide single-writer coordination. The hosting state helper does not lock
an entire run or resolve concurrent updates to that stable key.
The session id is a partition key, not proof of identity. App or platform code must authenticate and authorize any
externally supplied key before using it.
@@ -0,0 +1,88 @@
---
status: proposed
contact: cgillum
date: 2026-07-21
deciders: cgillum, vrdmr, chetantoshniwal
consulted:
informed:
---
# Extract Durable Task and Azure Functions hosting into a separate repository
## Context and Problem Statement
The Durable Task and Azure Functions hosting integrations (`agent-framework-durabletask`,
`agent-framework-azurefunctions`, plus their samples, docs, and CI) currently live in the
`microsoft/agent-framework` (MAF) monorepo. They carry heavyweight specialized dependencies
(Azure Functions runtime, Durable Task) and need integration-test infrastructure (Functions Core
Tools, Azurite, a DTS emulator) that the core repo otherwise does not.
This ADR proposes moving them into a dedicated repository
([`microsoft/agent-framework-durable-extension`](https://github.com/microsoft/agent-framework-durable-extension))
and considers how to do so without breaking existing users who import them today.
## Decision Drivers
- **Independent lifecycle** — the hosting integrations should be able to version and release on their
own cadence, decoupled from core (extends [ADR-0008](0008-python-subpackages.md)'s goal of keeping
heavyweight/optional dependencies out of the main package).
- **Dependency & CI isolation** — keep core lean and its PR pipeline free of heavyweight hosting
dependencies and integration-test prerequisites.
- **Ownership** — a dedicated repo would give the integrations their own issues, CODEOWNERS, and
contribution flow.
- **No breaking change** — existing `from agent_framework.azure import …` code and
`pip install agent-framework[all]` should keep working (stable-import-path guarantee, ADR-0008).
## Considered Options
1. **Keep in the MAF repo** (status quo).
2. **Move out, drop the core shim** — the extension becomes standalone; core stops re-exporting the
types and removes them from `[all]`.
3. **Move out, keep core's backward-compat shim + `[all]`** (proposed) — the code would live in the
new repo; core would still lazily re-export the entry-point types from `agent_framework.azure` and
keep both packages in the `[all]` extra (resolved from PyPI).
## Decision Outcome
Proposed choice: **Option 3.** Extract the integrations for lifecycle, dependency, and ownership
isolation, while preserving the existing import surface so the move is invisible to consumers.
Option 1 forgoes the isolation benefits; Option 2 achieves them but would be a breaking change for
existing imports and the `[all]` extra.
### Consequences
- Good — would give independent release cadence, a leaner/faster core repo and CI, and clear
ownership for the hosting integrations.
- Good — no user-visible break: existing imports and `agent-framework[all]` would continue to work
unchanged.
- Neutral — type *definitions* would live once in the extension; the core shim would re-export only a
curated subset of entry-point types (no metadata duplication). The extension's own samples/docs
would import directly from `agent_framework_durabletask` / `agent_framework_azurefunctions`; the
shim would be compatibility-only.
- Neutral — users may still open GitHub issues against the core repo for problems in the extension,
but the extension's own repo would be the primary place for issues and PRs. These issues would
need to be triaged and transferred to the extension repo.
- Bad — **cross-repo coupling.** Core's shim correctness would track the extension's publish cadence
(a shim symbol newer than the last published beta would not resolve until republished), and the
.NET extension would still consume internal `Microsoft.Agents.AI.Workflows` surface, so the
`InternalsVisibleTo("Microsoft.Agents.AI.DurableTask")` grant would need to remain in core.
## Validation
Compliance would be validated by: `uv lock --check` passing with both packages resolving from PyPI;
the shim entry-point symbols importing at runtime after `uv sync --all-extras`; and `pyright` staying
clean on `agent_framework/azure/__init__.pyi`.
A known risk is **publish-lag**: if a symbol is added to core's shim before the extension has
published a release that exports it, that symbol would not resolve at runtime. The mitigation would
be to omit any such symbol from the shim until the extension publishes it, then add the entry and
re-lock.
## More Information
- Related: [ADR-0008](0008-python-subpackages.md) (vendor namespaces + stable import paths),
[ADR-0021](0021-provider-leading-clients.md) (lazy-loading gateways).
- Follow-ups: during extraction, keep the shim's re-exported symbols in sync with each newly
published extension release (adding any symbol only once the extension publishes it); document the
direct-import convention in the extension's samples READMEs so samples are not switched back to the
shim.
+78 -1
View File
@@ -66,8 +66,10 @@ must be aligned with the helper-first model before implementation. Old vocabular
| Package | Import surface | v1 helper-first contents |
|---|---|---|
| `agent-framework-hosting` | `agent_framework_hosting` | `AgentState`, `WorkflowState`, `SessionStore`, and run-argument `TypedDict`s. |
| `agent-framework-hosting-a2a` | `agent_framework_hosting_a2a` | A2A `Message` to run conversion and Agent Framework output to A2A `Part` conversion. |
| `agent-framework-hosting-responses` | `agent_framework_hosting_responses` | Responses helpers: request parsing, session id extraction, response id creation, response rendering, streaming rendering. |
| Future protocol packages | e.g. `agent_framework_hosting_telegram` | Protocol-specific helpers such as `telegram_to_run(...)`, `telegram_from_run(...)`, `telegram_session_id(...)`, and command/media helpers when useful. |
| `agent-framework-hosting-telegram` | `agent_framework_hosting_telegram` | Telegram Bot API helpers: update parsing, chat/session/command/media extraction, final rendering, and streaming edit rendering. |
| Future protocol packages | e.g. `agent_framework_hosting_activity_protocol` | Protocol-specific helpers such as `activity_to_run(...)`, `activity_from_run(...)`, `activity_session_id(...)`, and command/media helpers when useful. |
The core hosting package must not depend on protocol SDKs. Protocol packages may depend on their native protocol SDKs if
needed, but helper functions should stay usable from plain app code and tests.
@@ -90,6 +92,7 @@ Examples:
- `responses_to_run(...)`, `responses_from_run(...)`, `responses_from_streaming_run(...)`,
`responses_session_id(...)`;
- `a2a_to_run(...)`, `a2a_from_run(...)`;
- `telegram_to_run(...)`, `telegram_from_run(...)`, `telegram_from_streaming_run(...)`,
`telegram_session_id(...)`, `telegram_command(...)`;
- `activity_to_run(...)`, `activity_from_run(...)`, `activity_session_id(...)`, `activity_command(...)`;
@@ -177,6 +180,9 @@ The target may be:
- `await get_target()`;
- synchronous `target` only after a target is already available/resolved.
A workflow instance permits one active run. Concurrent hosts use a factory or
builder with `cache_target=False` to resolve a fresh instance per run.
Workflow checkpointing uses Agent Framework's existing `CheckpointStorage` abstraction directly. Apps that need
per-session workflow resume should keep an app-owned cursor such as `session_id -> checkpoint_id`. When the app uses
file-backed cursor storage, the file-based checkpoint storage should share the same app storage root and should be
@@ -244,6 +250,74 @@ text deltas, and a completed event. The final completed payload is produced thro
also preserves the model id observed on streaming updates when the finalized `AgentResponse` no longer carries raw model
metadata.
## `agent-framework-hosting-a2a`
The A2A package provides only the conversion seam between the native A2A SDK
and Agent Framework:
- `a2a_to_run(message, *, stream=False) -> AgentRunArgs`
- `a2a_from_run(result) -> list[a2a.types.Part]`
`a2a_to_run(...)` accepts a native A2A `Message` and converts its text, URL,
raw-byte, and structured-data parts into one Agent Framework user message.
`a2a_from_run(...)` accepts an `AgentResponse`, `Message`, or
`AgentResponseUpdate` and converts supported text, URI, and data content into
native A2A `Part` values. This one helper is usable for both completed and
streaming runs.
The package does not provide an A2A `AgentExecutor`, application, route,
request handler, task store, event queue, `TaskUpdater`, task-state policy,
artifact-id policy, or session-key policy. Application code composes the two
helpers with those native A2A SDK constructs and may use any server framework
supported by the SDK.
## `agent-framework-hosting-telegram`
The Telegram package provides side-effect-free helpers around Telegram Bot API
update and method payloads. It does not provide a Bot API client, polling loop,
webhook route, command registry, retry policy, or rate limiter.
### Update helpers
- `telegram_to_run(update, *, resolve_file_url=None, stream=False) -> AgentRunArgs`
- `telegram_chat_id(update) -> int | None`
- `telegram_session_id(update, *, bot_id) -> str | None`
- `telegram_command(update) -> str | None`
- `telegram_callback_query_id(update) -> str | None`
- `telegram_media_file_id(update_or_message) -> tuple[str, str] | None`
`telegram_to_run(...)` handles `message`, `edited_message`, and
`callback_query` updates. Text and captions become AF text content. When the
app supplies an async `resolve_file_url` callback, supported Telegram media
file ids can become AF URI content. The package does not call Telegram's
`getFile` method itself.
`telegram_session_id(..., bot_id=...)` includes the bot identity in every key.
Private chats return `telegram:<bot_id>:<user_id>`; other chats return
`telegram:<bot_id>:<chat_id>`, giving groups a shared session by default. This
matches Telegram's native isolation boundaries while preventing two bots from
sharing state accidentally. Apps that want per-user sessions inside a group
can construct a key that includes both chat and sender ids. The app must
authorize those Telegram identities before loading session state.
`telegram_command(...)` parses Telegram's `/name` and `/name@bot` syntax. It
does not register commands or invoke handlers.
### Response helpers
- `telegram_from_run(result, *, chat_id, parse_mode=None)`
- `telegram_from_streaming_run(stream, *, chat_id, message_id, initial_text=None, parse_mode=None)`
The helpers produce Telegram method/payload values for app-owned Bot API
calls. Final rendering supports text and image URI output and applies
Telegram's text-length boundary. Streaming rendering produces cumulative
`editMessageText` payloads for a placeholder message id supplied by the app,
omitting edits that match an optional `initial_text`, then renders the final
rich output. Image-only responses remove the placeholder with `deleteMessage`
before sending the image. The app owns the initial placeholder send, Bot API
calls, edit throttling, retries, and failure policy.
## Security responsibilities
Protocol helper packages parse and render. They do not authenticate callers, authorize access to state, or decide which
@@ -346,3 +420,6 @@ Implementation validation must cover:
- Responses streaming SSE rendering;
- HTTP round-trip tests showing a native FastAPI route using `AgentState` and Responses helpers;
- sample type checking for the local Responses sample.
- Telegram update parsing, chat/session/command/media extraction, final
rendering, and streaming edit rendering;
- sample type checking for the local Telegram polling and webhook entry points.
+2 -2
View File
@@ -11,8 +11,8 @@
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="12.31.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.6.0" />
<PackageVersion Include="Anthropic" Version="12.35.1" />
<PackageVersion Include="Anthropic.Foundry" Version="0.7.1" />
<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" />
+1
View File
@@ -200,6 +200,7 @@
<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" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AgentProviders/openai/">
<File Path="samples/02-agents/AgentProviders/openai/README.md" />
+1
View File
@@ -28,6 +28,7 @@
"src\\Microsoft.Agents.AI.OpenAI\\Microsoft.Agents.AI.OpenAI.csproj",
"src\\Microsoft.Agents.AI.Purview\\Microsoft.Agents.AI.Purview.csproj",
"src\\Microsoft.Agents.AI.Tools.Shell\\Microsoft.Agents.AI.Tools.Shell.csproj",
"src\\Microsoft.Agents.AI.Valkey\\Microsoft.Agents.AI.Valkey.csproj",
"src\\Microsoft.Agents.AI.Workflows.Declarative.Foundry\\Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj",
"src\\Microsoft.Agents.AI.Workflows.Declarative.Mcp\\Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj",
"src\\Microsoft.Agents.AI.Workflows.Declarative\\Microsoft.Agents.AI.Workflows.Declarative.csproj",
+10 -1
View File
@@ -427,6 +427,15 @@ internal static class AgentsSamples
],
},
new SampleDefinition
{
Name = "AgentWithMemory_Step06_MemoryUsingAgentMemory",
ProjectPath = "samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_API_KEY", "FOUNDRY_MODEL", "FOUNDRY_EMBEDDING_MODEL", "NEO4J_URI", "NEO4J_USER", "NEO4J_PASSWORD"],
SkipReason = "Requires a running Neo4j instance; standalone sample outside the repo's CPM build.",
},
// ── AgentWithRAG ────────────────────────────────────────────────────
new SampleDefinition
@@ -837,7 +846,7 @@ internal static class AgentsSamples
ProjectPath = "samples/02-agents/Agents/Agent_Step15_DeepResearch",
RequiredEnvironmentVariables = ["AZURE_AI_PROJECT_ENDPOINT", "AZURE_AI_MODEL_DEPLOYMENT_NAME", "AZURE_AI_BING_CONNECTION_ID"],
OptionalEnvironmentVariables = ["AZURE_AI_REASONING_DEPLOYMENT_NAME"],
SkipReason = "Requires Azure AI Foundry project with Bing search connection.",
SkipReason = "Requires Microsoft Foundry project with Bing search connection.",
},
new SampleDefinition
+1 -1
View File
@@ -19,7 +19,7 @@
// Pre-build the solution before running, or pass --build to avoid missing build output failures.
//
// Required environment variables (for AI-powered verification):
// FOUNDRY_PROJECT_ENDPOINT — Your Azure AI Foundry project endpoint
// FOUNDRY_PROJECT_ENDPOINT — Your Microsoft Foundry project endpoint
// FOUNDRY_MODEL — Model deployment name (optional, defaults to gpt-5.4-mini)
using System.Diagnostics;
+1 -1
View File
@@ -130,7 +130,7 @@ internal static class WorkflowSamples
ProjectPath = "samples/03-workflows/Agents/FoundryAgent",
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
SkipReason = "Requires Azure AI Foundry project endpoint.",
SkipReason = "Requires Microsoft Foundry project endpoint.",
},
new SampleDefinition
@@ -66,6 +66,18 @@ When using multiple providers (e.g., skills + file access), combine their rules
})
```
## ⚠️ Security: avoid tool-name collisions
Built-in auto-approval rules match tool calls **solely by tool name**. A rule cannot tell the
provider's own tool apart from any other registered tool that happens to share the same name. If a
different tool — especially one with a caller-configurable name, such as the Harness shell tool
(`HarnessAgentOptions.ShellToolName`) — is registered under a name that one of these rules approves
(e.g. `load_skill`, `read_skill_resource`, `run_skill_script`, or the `file_access_*` names), that
tool will be **silently auto-approved**, bypassing the human approval boundary.
When using auto-approval rules, ensure no other tool's name collides with the reserved names the
rules approve, and never assign a configurable tool name that matches one of them.
## Skills Included
### unit-converter
@@ -0,0 +1,78 @@
<Project Sdk="Microsoft.NET.Sdk">
<!--
This project is part of the repo's solution and targets .NET 10 like the rest of the repo, but it
intentionally opts out of Central Package Management and source-referencing Microsoft.Agents.AI:
it consumes the *published* AgentMemory NuGet packages (which target Microsoft.Agents.AI 1.9.0)
instead. Run it with `dotnet run` from this folder.
ManagePackageVersionsCentrally is off, but dotnet/Directory.Packages.props still unconditionally
merges its repo-wide analyzer PackageReference items (no Version, resolved via CPM) into every
project that imports it — including this one. With CPM off here those versions can't resolve
(NU1015), so each is removed and re-added with an explicit version below (matching
AgentWithRAG_Step05_Neo4jGraphRAG, which hits the same issue). xunit.analyzers/Moq.Analyzers are
dropped rather than re-added since this project has no test code.
-->
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
<RootNamespace>AgentMemoryShoppingAssistant</RootNamespace>
<!-- OPENAI001: the OpenAIClient(AuthenticationPolicy, options) ctor used for keyless Azure auth is
marked experimental in the OpenAI SDK (the MAF Foundry samples use the same pattern). -->
<NoWarn>$(NoWarn);OPENAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Remove="Microsoft.CodeAnalysis.NetAnalyzers" />
<PackageReference Remove="Microsoft.VisualStudio.Threading.Analyzers" />
<PackageReference Remove="xunit.analyzers" />
<PackageReference Remove="Moq.Analyzers" />
<PackageReference Remove="Roslynator.Analyzers" />
<PackageReference Remove="Roslynator.CodeAnalysis.Analyzers" />
<PackageReference Remove="Roslynator.Formatting.Analyzers" />
</ItemGroup>
<ItemGroup>
<!-- AgentMemory (published) — an unofficial .NET port of the Neo4j Labs agent-memory library + its
Microsoft Agent Framework adapter. -->
<PackageReference Include="AgentMemory" Version="1.2.0" />
<PackageReference Include="AgentMemory.AgentFramework" Version="1.2.0" />
<!-- Microsoft Agent Framework (matches AgentMemory's target) + the OpenAI/Foundry chat & embedding clients. -->
<PackageReference Include="Microsoft.Agents.AI" Version="1.9.0" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.5.1" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.17" />
<!-- Transitive dependency of Microsoft.Agents.AI; pinned explicitly (CPM is off here) because the
version it would otherwise resolve to, 1.12.0, has a known moderate severity vulnerability
(GHSA-g94r-2vxg-569j) that fails the repo's NuGet audit (NU1902 as error). Matches the version
pinned in dotnet/Directory.Packages.props. -->
<PackageReference Include="OpenTelemetry.Api" Version="1.15.3" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.100">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
@@ -0,0 +1,195 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Text;
using AgentMemory.Neo4j.Infrastructure;
using Microsoft.Extensions.AI;
using Neo4j.Driver;
namespace AgentMemoryShoppingAssistant;
/// <summary>
/// A small retail product graph plus the shopping tools that query it — the .NET counterpart of the
/// Python retail-assistant's <c>get_product_tools</c>. Products live in Neo4j as <c>:Product</c> nodes
/// linked to <c>:ProductCategory</c> / <c>:ProductBrand</c> nodes, so recommendations and "related
/// products" come from graph traversals. Cypher runs through the public <see cref="INeo4jTransactionRunner"/>
/// seam. Exposed as <see cref="AIFunction"/>s so a real chat model can call them during a run — the same
/// way <c>Neo4jMemoryContextProvider</c> surfaces the memory tools through <c>AIContext.Tools</c> when
/// <c>ExposeMemoryToolsFromContextProvider</c> is enabled.
/// </summary>
public sealed class ProductCatalog(INeo4jTransactionRunner runner)
{
private readonly INeo4jTransactionRunner _runner = runner;
private static readonly (string Name, string Category, string Brand, double Price, bool InStock, int Inventory, string Description, int Popularity)[] s_seed =
[
("Nike Air Zoom Pegasus 40", "shoes", "Nike", 130, true, 40, "Everyday running shoe with responsive cushioning.", 95),
("Nike Revolution 7", "shoes", "Nike", 70, true, 60, "Lightweight, budget-friendly running shoe.", 80),
("Adidas Ultraboost Light", "shoes", "Adidas", 190, true, 25, "Premium running shoe with Boost cushioning.", 90),
("Asics Gel-Kayano 31", "shoes", "Asics", 165, false, 0, "Stability running shoe for overpronation.", 70),
("Sony WH-1000XM5", "electronics", "Sony", 350, true, 18, "Industry-leading noise-cancelling headphones.", 92),
("Bose QuietComfort Ultra", "electronics", "Bose", 330, true, 12, "Premium noise-cancelling over-ear headphones.", 85),
("Apple AirPods Pro 2", "electronics", "Apple", 250, true, 50, "Wireless earbuds with active noise cancellation.", 88),
("Garmin Forerunner 265", "electronics", "Garmin", 450, true, 9, "GPS running watch with training metrics.", 78),
("Nike Dri-FIT Running Tee", "apparel", "Nike", 35, true, 120, "Breathable, moisture-wicking running shirt.", 65),
("Adidas Own the Run Jacket","apparel", "Adidas", 80, true, 33, "Lightweight, water-repellent running jacket.", 60),
];
/// <summary>Seeds the sample product graph (idempotent — safe to run every start).</summary>
public Task SeedAsync(CancellationToken ct = default) => this._runner.WriteAsync(async r =>
{
await r.RunAsync(
"""
UNWIND $products AS row
MERGE (p:Product {name: row.name})
SET p.category = row.category, p.brand = row.brand, p.price = row.price,
p.in_stock = row.in_stock, p.inventory = row.inventory,
p.description = row.description, p.popularity = row.popularity
MERGE (c:ProductCategory {name: row.category})
MERGE (b:ProductBrand {name: row.brand})
MERGE (p)-[:IN_CATEGORY]->(c)
MERGE (p)-[:MADE_BY]->(b)
""",
new
{
products = s_seed.Select(p => (object)new Dictionary<string, object>
{
["name"] = p.Name, ["category"] = p.Category, ["brand"] = p.Brand, ["price"] = p.Price,
["in_stock"] = p.InStock, ["inventory"] = p.Inventory, ["description"] = p.Description,
["popularity"] = p.Popularity,
}).ToList(),
});
}, ct);
// ── Tools (also usable directly in the scripted demo) ────────────────────────────────────────
[Description("Search the product catalog for items matching a query, with optional category, brand, and max-price filters.")]
public Task<string> SearchProductsAsync(
[Description("What the customer is looking for, e.g. 'running shoes'.")] string query,
[Description("Optional category filter: shoes, electronics, apparel.")] string? category = null,
[Description("Optional brand filter, e.g. 'Nike'.")] string? brand = null,
[Description("Optional maximum price.")] double? maxPrice = null,
CancellationToken ct = default) => this._runner.ReadAsync(async r =>
{
const string Cypher =
"""
MATCH (p:Product)
WHERE ANY(w IN split(toLower($query), ' ') WHERE
toLower(p.name) CONTAINS w OR toLower(p.description) CONTAINS w OR toLower(p.category) CONTAINS w)
AND ($category IS NULL OR p.category = $category)
AND ($brand IS NULL OR p.brand = $brand)
AND ($maxPrice IS NULL OR p.price <= $maxPrice)
RETURN p.name AS name, p.brand AS brand, p.category AS category,
p.price AS price, p.in_stock AS inStock
ORDER BY p.popularity DESC
LIMIT 10
""";
var cursor = await r.RunAsync(Cypher, new { query, category, brand, maxPrice });
return Render("Matches", await cursor.ToListAsync());
}, ct);
[Description("Get personalized product recommendations, optionally biased toward a preferred brand and/or category.")]
public Task<string> GetRecommendationsAsync(
[Description("The customer's preferred brand (from their saved preferences), if known.")] string? preferredBrand = null,
[Description("Optional category to recommend within.")] string? category = null,
[Description("How many recommendations to return.")] int limit = 5,
CancellationToken ct = default) => this._runner.ReadAsync(async r =>
{
const string Cypher =
"""
MATCH (p:Product)
WHERE p.in_stock = true
AND ($category IS NULL OR p.category = $category)
WITH p, (CASE WHEN $preferredBrand IS NOT NULL AND p.brand = $preferredBrand THEN 1 ELSE 0 END) AS onBrand
RETURN p.name AS name, p.brand AS brand, p.category AS category, p.price AS price, p.in_stock AS inStock
ORDER BY onBrand DESC, p.popularity DESC
LIMIT $limit
""";
var cursor = await r.RunAsync(Cypher, new { preferredBrand, category, limit });
var header = preferredBrand is null ? "Recommended for you" : $"Recommended for you (favoring {preferredBrand})";
return Render(header, await cursor.ToListAsync());
}, ct);
[Description("Find products related to a given product — same category or same brand — via graph traversal.")]
public Task<string> GetRelatedProductsAsync(
[Description("The exact product name to find related items for.")] string productName,
CancellationToken ct = default) => this._runner.ReadAsync(async r =>
{
const string Cypher =
"""
MATCH (p:Product {name: $productName})
CALL (p) {
MATCH (p)-[:IN_CATEGORY]->(c)<-[:IN_CATEGORY]-(rel:Product) WHERE rel <> p
RETURN rel, 'same category' AS reason
UNION
MATCH (p)-[:MADE_BY]->(b)<-[:MADE_BY]-(rel:Product) WHERE rel <> p
RETURN rel, 'same brand' AS reason
}
WITH rel, collect(DISTINCT reason) AS reasons
RETURN rel.name AS name, rel.brand AS brand, rel.category AS category,
rel.price AS price, rel.in_stock AS inStock, rel.popularity AS popularity,
reduce(s = '', x IN reasons | CASE WHEN s = '' THEN x ELSE s + ', ' + x END) AS reason
ORDER BY popularity DESC
LIMIT 5
""";
var cursor = await r.RunAsync(Cypher, new { productName });
return Render($"Related to {productName}", await cursor.ToListAsync());
}, ct);
[Description("Check whether a product is in stock and how many units are available.")]
public Task<string> CheckInventoryAsync(
[Description("The exact product name to check.")] string productName,
CancellationToken ct = default) => this._runner.ReadAsync(async r =>
{
var cursor = await r.RunAsync(
"MATCH (p:Product {name: $productName}) RETURN p.name AS name, p.in_stock AS inStock, p.inventory AS inventory",
new { productName });
var rows = await cursor.ToListAsync();
if (rows.Count == 0)
{
return $"'{productName}' was not found in the catalog.";
}
var rec = rows[0];
var inStock = rec["inStock"].As<bool>();
return inStock
? $"{rec["name"].As<string>()}: In stock ({rec["inventory"].As<long>()} available)."
: $"{rec["name"].As<string>()}: Out of stock.";
}, ct);
/// <summary>The retail tools as MAF/MEAI <see cref="AIFunction"/>s (attach to the agent's ChatOptions.Tools).</summary>
public IReadOnlyList<AIFunction> CreateAIFunctions() =>
[
AIFunctionFactory.Create(this.SearchProductsAsync, "search_products",
"Search the product catalog with optional category/brand/price filters."),
AIFunctionFactory.Create(this.GetRecommendationsAsync, "get_recommendations",
"Get personalized recommendations, optionally favoring a preferred brand/category."),
AIFunctionFactory.Create(this.GetRelatedProductsAsync, "get_related_products",
"Find products related to a given product via the graph."),
AIFunctionFactory.Create(this.CheckInventoryAsync, "check_inventory",
"Check stock/availability for a product."),
];
private static string Render(string header, List<IRecord> rows)
{
if (rows.Count == 0)
{
return $"{header}: (no matches)";
}
var sb = new StringBuilder().Append(header).Append(':').AppendLine();
foreach (var rec in rows)
{
var stock = rec["inStock"].As<bool>() ? "in stock" : "out of stock";
var reason = rec.Keys.Contains("reason") ? $" [{rec["reason"].As<string>()}]" : string.Empty;
sb.Append(" • ")
.Append(rec["name"].As<string>())
.Append(" — ").Append(rec["brand"].As<string>())
.Append(", ").Append(rec["category"].As<string>())
.Append(", $").Append(rec["price"].As<double>().ToString("0"))
.Append(", ").Append(stock).Append(reason)
.AppendLine();
}
return sb.ToString().TrimEnd();
}
}
@@ -0,0 +1,156 @@
// Copyright (c) Microsoft. All rights reserved.
// Agent Memory — Shopping Assistant (Microsoft Agent Framework, .NET)
//
// A .NET port of the Neo4j Labs "agent-memory" retail-assistant example
// (https://github.com/neo4j-labs/agent-memory/tree/main/examples/microsoft_agent_retail_assistant,
// referenced from https://learn.microsoft.com/en-us/agent-framework/integrations/neo4j-memory).
//
// A shopping assistant that LEARNS a customer's preferences and RECOMMENDS products via graph
// traversal, backed by DURABLE memory in Neo4j. It uses the AgentMemory library — a .NET port of the
// Python memory provider, not an officially recognized Neo4j integration — and its Microsoft Agent
// Framework adapter:
// • Neo4jMemoryContextProvider (an AIContextProvider) — recalls memory before each run, persists
// after, and (via ExposeMemoryToolsFromContextProvider) surfaces the memory tools (search/remember/
// recall) itself through AIContext.Tools
// • ProductCatalog.CreateAIFunctions() — retail tools over a Neo4j :Product graph
//
// Configuration (environment variables, matching the other Foundry samples):
// AZURE_OPENAI_ENDPOINT (required) — your Azure OpenAI / Foundry endpoint
// AZURE_OPENAI_API_KEY (optional) — API key; if unset, DefaultAzureCredential (az login) is used
// FOUNDRY_MODEL (default: gpt-4o-mini) — chat model deployment
// FOUNDRY_EMBEDDING_MODEL (default: text-embedding-3-small) — embedding model deployment (1536 dims)
// NEO4J_URI (default: bolt://localhost:7687)
// NEO4J_USER (default: neo4j)
// NEO4J_PASSWORD (default: password)
using System.ClientModel;
using System.ClientModel.Primitives;
using AgentMemory.Abstractions.Services;
using AgentMemory.AgentFramework;
using AgentMemory.Core;
using AgentMemory.Core.Stubs;
using AgentMemory.Neo4j.Infrastructure;
using AgentMemoryShoppingAssistant;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OpenAI;
// ── Model + credentials (Azure OpenAI / Foundry, via env vars) ───────────────────────────────────
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
var chatModel = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o-mini";
var embeddingModel = Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_MODEL") ?? "text-embedding-3-small";
var clientOptions = new OpenAIClientOptions { Endpoint = new Uri(endpoint) };
// API key if provided, otherwise Azure credential (dev: `az login`).
// 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.
OpenAIClient openAI = string.IsNullOrWhiteSpace(apiKey)
? new OpenAIClient(new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), clientOptions)
: new OpenAIClient(new ApiKeyCredential(apiKey), clientOptions);
IChatClient chatClient = openAI.GetChatClient(chatModel).AsIChatClient();
IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator =
openAI.GetEmbeddingClient(embeddingModel).AsIEmbeddingGenerator();
// ── AgentMemory (Neo4j) DI ───────────────────────────────────────────────────────────────────────
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.SetMinimumLevel(LogLevel.Warning);
builder.Services.AddNeo4jAgentMemory(options =>
{
options.Uri = Environment.GetEnvironmentVariable("NEO4J_URI") ?? "bolt://localhost:7687";
options.Username = Environment.GetEnvironmentVariable("NEO4J_USER") ?? "neo4j";
options.Password = Environment.GetEnvironmentVariable("NEO4J_PASSWORD") ?? "password";
});
builder.Services.AddAgentMemoryCore(_ => { });
builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddSingleton<IIdGenerator, GuidIdGenerator>();
builder.Services.TryAddSingleton(chatClient);
builder.Services.TryAddSingleton(embeddingGenerator);
builder.Services.AddAgentMemoryFramework(options =>
{
options.AutoExtractOnPersist = true;
options.ContextFormat.IncludeEntities = true;
options.ContextFormat.IncludeFacts = true;
options.ContextFormat.IncludePreferences = true;
options.ExposeMemoryToolsFromContextProvider = true;
});
var host = builder.Build();
await using var hostDisposal = (IAsyncDisposable)host;
await using var scope = host.Services.CreateAsyncScope();
var sp = scope.ServiceProvider;
// ── Setup: schema + sample product graph ─────────────────────────────────────────────────────────
var catalog = new ProductCatalog(sp.GetRequiredService<INeo4jTransactionRunner>());
await sp.GetRequiredService<ISchemaBootstrapper>().BootstrapAsync();
await catalog.SeedAsync();
Console.WriteLine("Neo4j schema ready; sample products loaded.\n");
// ── The shopping assistant: context provider (recall + memory tools) + product tools ─────────────
var memoryProvider = sp.GetRequiredService<Neo4jMemoryContextProvider>();
var productTools = catalog.CreateAIFunctions();
// WithMemoryOwnerScoping(sp) scopes the whole invocation (recall, tool calls, persistence) to the
// owner set via WithMemoryIdentity below — no manual BeginOwnerScope wrapping needed per turn.
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
Name = "ShoppingAssistant",
ChatOptions = new ChatOptions
{
ModelId = chatModel,
Instructions =
"You are a helpful shopping assistant for an online store. Learn and remember the customer's "
+ "preferences (brands, budget, categories) using the memory tools, and recommend products that "
+ "fit using the product tools. Explain why each recommendation matches, and suggest alternatives "
+ "when something is out of stock.",
// memoryProvider appends the six memory tools (search_memory, remember_fact, ...) to this list
// on every model call via AIContext.Tools — see ExposeMemoryToolsFromContextProvider above.
Tools = [.. productTools],
},
AIContextProviders = [memoryProvider],
}).WithMemoryOwnerScoping(sp);
const string Shopper = "shopper-amelia";
// ── Session A — the customer shops; the model calls the tools and remembers preferences ──────────
Console.WriteLine(">> Session A\n");
var sessionA = (await agent.CreateSessionAsync())
.WithMemoryIdentity(userId: Shopper, sessionId: "cart-a", applicationId: "retail-demo");
foreach (var turn in new[]
{
"Hi! I'm looking for running shoes. I love Nike and want to stay under $150.",
"Nice — what would you recommend for me, and is anything I might like out of stock?",
})
{
await SayAsync(agent, sessionA, turn);
}
// ── Session B — a NEW session for the same shopper still recalls her preferences ─────────────────
Console.WriteLine(">> Session B — a brand-new session; memory is durable\n");
var sessionB = (await agent.CreateSessionAsync())
.WithMemoryIdentity(userId: Shopper, sessionId: "cart-b", applicationId: "retail-demo");
await SayAsync(agent, sessionB, "I'm back — remind me what I like and suggest something new.");
Console.WriteLine("=== Done. Preferences + messages persist in Neo4j across sessions. ===");
// One conversational turn. Owner scoping (recall, tool calls, and persistence) is guaranteed
// automatically by the WithMemoryOwnerScoping-wrapped agent — no manual BeginOwnerScope needed here.
static async Task SayAsync(AIAgent agent, AgentSession session, string message)
{
Console.WriteLine($"USER : {message}");
var response = await agent.RunAsync(message, session);
Console.WriteLine($"ASSISTANT : {response.Text}\n");
}
@@ -0,0 +1,75 @@
# Agent with Memory Using AgentMemory — Shopping Assistant
A **.NET port of the Neo4j Labs "agent-memory" retail assistant** example
([`microsoft_agent_retail_assistant`](https://github.com/neo4j-labs/agent-memory/tree/main/examples/microsoft_agent_retail_assistant),
referenced from the [Learn integration page](https://learn.microsoft.com/en-us/agent-framework/integrations/neo4j-memory)).
A shopping assistant that **learns a customer's preferences** and **recommends products via graph
traversal**, backed by durable memory in Neo4j.
It uses the [`AgentMemory`](https://www.nuget.org/packages/AgentMemory) library — a .NET port of the
(Python-only) Neo4j Labs memory provider, **not an officially recognized Neo4j integration** — through
its Microsoft Agent Framework adapter.
## Features Demonstrated
- **`Neo4jMemoryContextProvider`** (an `AIContextProvider`) — recalls relevant memory before each run,
persists new memory after (the same bidirectional pattern as the official provider), and — via
`ExposeMemoryToolsFromContextProvider = true` — surfaces the memory tools (search / remember / recall)
itself through `AIContext.Tools`.
- **`ProductCatalog.CreateAIFunctions()`** — retail tools over a Neo4j `:Product` graph (search /
recommend / related / inventory).
- Preference learning that persists across a brand-new `AgentSession` for the same shopper.
- Graph-based product recommendations and "related products" via traversal.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- A **Neo4j 5.x** instance (the sample bootstraps the schema and seeds sample products)
- An **Azure OpenAI / Foundry** deployment (a chat model + an embedding model)
## Configuration
Set the following environment variables:
| Variable | Required | Default | Purpose |
|---|---|---|---|
| `AZURE_OPENAI_ENDPOINT` | ✅ | — | Azure OpenAI / Foundry endpoint |
| `AZURE_OPENAI_API_KEY` | — | — | API key; if unset, `DefaultAzureCredential` (`az login`) is used |
| `FOUNDRY_MODEL` | — | `gpt-4o-mini` | chat model deployment |
| `FOUNDRY_EMBEDDING_MODEL` | — | `text-embedding-3-small` | embedding model deployment (1536 dims) |
| `NEO4J_URI` | — | `bolt://localhost:7687` | Neo4j bolt URI |
| `NEO4J_USER` | — | `neo4j` | Neo4j user |
| `NEO4J_PASSWORD` | — | `password` | Neo4j password |
> Ensure the embedding model's dimensions match the Neo4j vector-index dimensions AgentMemory bootstraps
> (default 1536, which matches `text-embedding-3-small`).
## Run the Sample
```bash
docker run -d --name neo4j -p 7474:7474 -p 7687:7687 -e NEO4J_AUTH=neo4j/password neo4j:5.26
export AZURE_OPENAI_ENDPOINT="https://<your-resource>.openai.azure.com"
export AZURE_OPENAI_API_KEY="<your-key>" # or omit and `az login`
export FOUNDRY_MODEL="gpt-4o-mini"
dotnet run
```
## Expected Output
1. The sample bootstraps the Neo4j schema and seeds a small product graph (`:Product`,
`:ProductCategory`, `:ProductBrand` nodes).
2. **Session A** — the shopper says she wants running shoes, loves Nike, and has a $150 budget; the
agent calls the memory tools to remember this and the product tools to recommend matching items.
3. **Session B** — a brand-new session for the same shopper (`shopper-amelia`) still recalls her
preferences and can suggest something new, because memory persists in Neo4j across sessions.
## Note on packaging
This sample is part of the repo's solution and targets .NET 10 like every other sample, but it
deliberately opts out of **Central Package Management** and does **not** reference `Microsoft.Agents.AI`
via the repo's in-source project — it consumes the **published** `AgentMemory` NuGet packages instead
(which target `Microsoft.Agents.AI` 1.9.0). A version that references the repo's current
`Microsoft.Agents.AI` source would require AgentMemory to be rebuilt against that version first.
@@ -9,6 +9,7 @@ These samples show how to create an agent with the Agent Framework that uses Mem
|[Custom Memory Implementation](../../01-get-started/04_memory/)|This sample demonstrates how to create a custom memory component and attach it to an agent.|
|[Memory with Microsoft Foundry](./AgentWithMemory_Step04_MemoryUsingFoundry/)|This sample demonstrates how to create and run an agent that uses Microsoft Foundry's managed memory service to extract and retrieve individual memories.|
|[Bounded Chat History with Overflow](./AgentWithMemory_Step05_BoundedChatHistory/)|This sample demonstrates how to create a bounded chat history provider that overflows older messages to a vector store and recalls them as memories.|
|[Memory Using AgentMemory](./AgentWithMemory_Step06_MemoryUsingAgentMemory/)|This sample demonstrates a retail shopping assistant built with [`AgentMemory`](https://www.nuget.org/packages/AgentMemory), an unofficial .NET port of the Neo4j Labs graph-memory provider, to learn customer preferences and recommend products via graph traversal.|
> **See also**: [Memory Search with Foundry Agents](../AgentProviders/foundry/Agent_Step22_MemorySearch/) - demonstrates using the built-in Memory Search tool with Microsoft Foundry agents.
@@ -24,7 +24,7 @@ AIProjectClient aiProjectClient = new(
new Uri(endpoint),
new DefaultAzureCredential());
// Create an In-Memory vector store that uses the Azure AI Foundry embedding model to generate embeddings.
// Create an In-Memory vector store that uses the Microsoft Foundry embedding model to generate embeddings.
VectorStore vectorStore = new InMemoryVectorStore(new()
{
EmbeddingGenerator = aiProjectClient.GetProjectOpenAIClient().GetEmbeddingClient(embeddingDeploymentName).AsIEmbeddingGenerator()
@@ -25,7 +25,7 @@ AIProjectClient aiProjectClient = new(
new Uri(endpoint),
new DefaultAzureCredential());
// Create a Qdrant vector store that uses the Azure AI Foundry embedding model to generate embeddings.
// Create a Qdrant vector store that uses the Microsoft Foundry embedding model to generate embeddings.
QdrantClient client = new("localhost");
VectorStore vectorStore = new QdrantVectorStore(client, ownsClient: true, new()
{
@@ -3,7 +3,7 @@
// 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.
// structured output using JSON schema constraints with Microsoft Foundry.
using System.ComponentModel;
using System.Text.Json;
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
// Agent Observability — OpenTelemetry tracing with Azure AI Foundry
// Agent Observability — OpenTelemetry tracing with Microsoft Foundry
//
// This sample shows how to instrument an AI agent with OpenTelemetry
// for distributed tracing and telemetry logging.
@@ -19,7 +19,7 @@ var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt
// Create a host builder that we will register services with and then run.
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Create the AI agent from the Azure AI Foundry project client.
// Create the AI agent from the Microsoft 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.
@@ -2,7 +2,7 @@
// Middleware — Chain multiple middleware layers on an agent
//
// This sample shows multiple middleware layers working together with Azure AI Foundry:
// This sample shows multiple middleware layers working together with Microsoft Foundry:
// chat client (global/per-request), agent run (PII filtering and guardrails),
// function invocation (logging and result overrides), human-in-the-loop
// approval workflows for sensitive function calls, and MessageAIContextProvider
@@ -15,7 +15,7 @@ using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
// Get Azure AI Foundry configuration from environment variables
// Get Microsoft Foundry configuration from environment variables
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";
@@ -3,7 +3,7 @@
// Background Responses — Asynchronous agent execution with polling
//
// This sample shows how to use background responses with ChatClientAgent
// and Azure AI Foundry for non-blocking agent execution.
// and Microsoft Foundry for non-blocking agent execution.
using Azure.AI.Projects;
using Azure.Identity;
@@ -67,7 +67,7 @@ static string GetTime([Description("The city name.")] string city) =>
// asking for alternative destinations. The model will process this injected message on the next
// service call — even though the parent FunctionInvokingChatClient loop would otherwise stop.
[Description("Check current travel advisories for a city.")]
static string CheckTravelAdvisory([Description("The city name.")] string city)
static async Task<string> CheckTravelAdvisory([Description("The city name.")] string city)
{
// Simulated travel advisory data.
var advisory = city.ToUpperInvariant() switch
@@ -85,9 +85,13 @@ static string CheckTravelAdvisory([Description("The city name.")] string city)
// When an advisory is found, inject a follow-up question so the model automatically
// suggests alternatives without the user needing to ask.
var runContext = AIAgent.CurrentRunContext!;
runContext.Agent.GetService<MessageInjectingChatClient>()?.EnqueueMessages(
runContext.Session!,
[new ChatMessage(ChatRole.User, $"Given the travel advisory for {city}, what alternative cities would you recommend instead?")]);
var injector = runContext.Agent.GetService<MessageInjectingChatClient>();
if (injector is not null)
{
await injector.EnqueueMessagesAsync(
runContext.Session!,
[new ChatMessage(ChatRole.User, $"Given the travel advisory for {city}, what alternative cities would you recommend instead?")]);
}
return advisory;
}
+1 -1
View File
@@ -18,7 +18,7 @@ Before you begin, ensure you have the following prerequisites:
- Azure CLI installed and authenticated (for Azure credential authentication)
- User has the required role to invoke models in the Foundry project.
**Note**: These samples use models hosted through Microsoft Foundry. For more information, see [Azure AI Foundry documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/).
**Note**: These samples use models hosted through Microsoft Foundry. For more information, see [Microsoft Foundry documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/).
**Note**: These samples use Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Foundry project. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
@@ -12,7 +12,7 @@ The simplest agent evaluation: create a Foundry agent, run it against test quest
- .NET 10 SDK or later
- Azure authentication available to `DefaultAzureCredential` (for local development, run `az login`)
- A deployed model in your Azure AI Foundry project
- A deployed model in your Microsoft Foundry project
Set the following environment variables:
@@ -14,6 +14,12 @@ It builds on Post 1's personal finance assistant and teaches it to work with *yo
saving and deleting still pause for approval. The `place_trade` tool is also wrapped in an
`ApprovalRequiredAIFunction` (see `TradingTools.cs`), so the harness surfaces an approval prompt
before any trade runs. The trade itself is simulated — no real order is placed.
> ⚠️ **Security — avoid tool-name collisions:** auto-approval rules such as
> `FileAccessProvider.ReadOnlyToolsAutoApprovalRule` match tool calls **solely by tool name**. Any
> other registered tool that shares one of the approved names (`file_access_read`, `file_access_ls`,
> `file_access_grep`) would be silently auto-approved, bypassing the human
> approval boundary. Ensure no other tool's name collides with the reserved names a rule approves.
- **Durable memory, two ways:**
- **File memory** (coarse-grained, explicit) — the agent reads/writes files such as
`watchlist.md`. File memory is on by default; its files live on disk under
@@ -43,7 +43,7 @@ public sealed class ModeCommandHandler : CommandHandler
string[] parts = input.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length < 2)
{
string current = this._modeProvider.GetMode(session);
string current = await this._modeProvider.GetModeAsync(session).ConfigureAwait(false);
await ux.WriteInfoLineAsync($"Current mode: {current}").ConfigureAwait(false);
return true;
}
@@ -52,7 +52,7 @@ public sealed class ModeCommandHandler : CommandHandler
try
{
this._modeProvider.SetMode(session, newMode);
await this._modeProvider.SetModeAsync(session, newMode).ConfigureAwait(false);
ux.CurrentMode = newMode;
await ux.WriteInfoLineAsync($"Switched to {newMode} mode.", ModeColors.Get(newMode, this._modeColors)).ConfigureAwait(false);
}
@@ -93,7 +93,7 @@ public sealed class HarnessAgentRunner : IDisposable
{
if (await handler.TryHandleAsync(text, this._session, this._ux).ConfigureAwait(false))
{
this._ux.CurrentMode = this._modeProvider?.GetMode(this._session);
this._ux.CurrentMode = this._modeProvider is null ? null : await this._modeProvider.GetModeAsync(this._session).ConfigureAwait(false);
return;
}
}
@@ -111,16 +111,15 @@ public sealed class HarnessAgentRunner : IDisposable
/// enqueued via the <see cref="MessageInjectingChatClient"/> so it can be picked up
/// by the agent on its next opportunity.
/// </summary>
internal Task OnStreamingInputAsync(string text)
internal async Task OnStreamingInputAsync(string text)
{
if (this._messageInjector is null)
{
return Task.CompletedTask;
return;
}
this._messageInjector.EnqueueMessages(this._session, [new ChatMessage(ChatRole.User, text)]);
this._ux.SetQueuedMessages(this._messageInjector.GetPendingMessages(this._session));
return Task.CompletedTask;
await this._messageInjector.EnqueueMessagesAsync(this._session, [new ChatMessage(ChatRole.User, text)]).ConfigureAwait(false);
this._ux.SetQueuedMessages(await this._messageInjector.GetPendingMessagesAsync(this._session).ConfigureAwait(false));
}
/// <summary>
@@ -136,7 +135,7 @@ public sealed class HarnessAgentRunner : IDisposable
{
if (messages.Count == 0)
{
this.CompleteTurn();
await this.CompleteTurnAsync().ConfigureAwait(false);
return;
}
@@ -151,17 +150,19 @@ public sealed class HarnessAgentRunner : IDisposable
private async Task RunAgentLoopAsync(IList<ChatMessage> messages)
{
IList<ChatMessage>? nextMessages = messages;
IReadOnlyList<ChatMessage> lastPendingMessages = this._messageInjector?.GetPendingMessages(this._session) ?? [];
IReadOnlyList<ChatMessage> lastPendingMessages = this._messageInjector is not null
? await this._messageInjector.GetPendingMessagesAsync(this._session).ConfigureAwait(false)
: [];
while (nextMessages is not null)
{
var runOptions = new AgentRunOptions();
foreach (var observer in this._observers)
{
observer.ConfigureRunOptions(runOptions, this._agent, this._session);
await observer.ConfigureRunOptionsAsync(runOptions, this._agent, this._session).ConfigureAwait(false);
}
this._ux.CurrentMode = this._modeProvider?.GetMode(this._session);
this._ux.CurrentMode = this._modeProvider is null ? null : await this._modeProvider.GetModeAsync(this._session).ConfigureAwait(false);
this._ux.BeginStreaming();
this._ux.BeginStreamingOutput();
@@ -171,7 +172,7 @@ public sealed class HarnessAgentRunner : IDisposable
{
if (this._modeProvider is not null)
{
string currentMode = this._modeProvider.GetMode(this._session);
string currentMode = await this._modeProvider.GetModeAsync(this._session).ConfigureAwait(false);
if (currentMode != this._ux.CurrentMode)
{
this._ux.CurrentMode = currentMode;
@@ -199,7 +200,7 @@ public sealed class HarnessAgentRunner : IDisposable
}
}
this.SyncQueuedMessageDisplay(ref lastPendingMessages);
lastPendingMessages = await this.SyncQueuedMessageDisplayAsync(lastPendingMessages).ConfigureAwait(false);
}
}
catch (Exception ex)
@@ -208,7 +209,7 @@ public sealed class HarnessAgentRunner : IDisposable
}
// Final sync after streaming.
this.SyncQueuedMessageDisplay(ref lastPendingMessages);
lastPendingMessages = await this.SyncQueuedMessageDisplayAsync(lastPendingMessages).ConfigureAwait(false);
this._ux.StopSpinner();
await this._ux.EndStreamingOutputAsync().ConfigureAwait(false);
@@ -261,13 +262,13 @@ public sealed class HarnessAgentRunner : IDisposable
nextMessages = drained.Count > 0 ? [.. drained] : null;
}
this.CompleteTurn();
await this.CompleteTurnAsync().ConfigureAwait(false);
}
private void CompleteTurn()
private async Task CompleteTurnAsync()
{
this._ux.EndStreaming();
this._ux.CurrentMode = this._modeProvider?.GetMode(this._session);
this._ux.CurrentMode = this._modeProvider is null ? null : await this._modeProvider.GetModeAsync(this._session).ConfigureAwait(false);
}
/// <summary>
@@ -275,14 +276,15 @@ public sealed class HarnessAgentRunner : IDisposable
/// Messages that have been consumed (drained by the service) are echoed to the output
/// area as regular user-input entries.
/// </summary>
private void SyncQueuedMessageDisplay(ref IReadOnlyList<ChatMessage> lastPendingMessages)
/// <returns>The updated snapshot of pending messages.</returns>
private async Task<IReadOnlyList<ChatMessage>> SyncQueuedMessageDisplayAsync(IReadOnlyList<ChatMessage> lastPendingMessages)
{
if (this._messageInjector is null)
{
return;
return lastPendingMessages;
}
var pending = this._messageInjector.GetPendingMessages(this._session);
var pending = await this._messageInjector.GetPendingMessagesAsync(this._session).ConfigureAwait(false);
int consumedCount = lastPendingMessages.Count - pending.Count;
for (int i = 0; i < consumedCount && i < lastPendingMessages.Count; i++)
@@ -291,7 +293,7 @@ public sealed class HarnessAgentRunner : IDisposable
this._ux.WriteUserInputEcho(text);
}
lastPendingMessages = pending;
this._ux.SetQueuedMessages(pending);
return pending;
}
}
@@ -40,9 +40,11 @@ public static class HarnessConsole
? await options.SessionFactory(agent)
: await agent.CreateSessionAsync();
string? initialMode = modeProvider is null ? null : await modeProvider.GetModeAsync(session);
using var component = new HarnessAppComponent(
placeholder: userPrompt,
initialMode: modeProvider?.GetMode(session),
initialMode: initialMode,
inputEnabled: messageInjector is not null,
runnerFactory: ux => new HarnessAgentRunner(
agent: agent,
@@ -20,9 +20,7 @@ public abstract class ConsoleObserver
/// <param name="options">The run options to configure.</param>
/// <param name="agent">The agent being interacted with.</param>
/// <param name="session">The current agent session.</param>
public virtual void ConfigureRunOptions(AgentRunOptions options, AIAgent agent, AgentSession session)
{
}
public virtual ValueTask ConfigureRunOptionsAsync(AgentRunOptions options, AIAgent agent, AgentSession session) => default;
/// <summary>
/// Called for each <see cref="AgentResponseUpdate"/> in the response stream, regardless of
@@ -40,9 +40,9 @@ public sealed class PlanningOutputObserver : ConsoleObserver
}
/// <inheritdoc/>
public override void ConfigureRunOptions(AgentRunOptions options, AIAgent agent, AgentSession session)
public override async ValueTask ConfigureRunOptionsAsync(AgentRunOptions options, AIAgent agent, AgentSession session)
{
if (this.IsPlanningMode(this._modeProvider.GetMode(session)))
if (this.IsPlanningMode(await this._modeProvider.GetModeAsync(session).ConfigureAwait(false)))
{
options.ResponseFormat = ChatResponseFormat.ForJsonSchema<PlanningResponse>();
}
@@ -205,7 +205,7 @@ public sealed class PlanningOutputObserver : ConsoleObserver
if (selection == ApproveOption)
{
this._modeProvider.SetMode(session, this._executionModeName);
await this._modeProvider.SetModeAsync(session, this._executionModeName).ConfigureAwait(false);
await ux.WriteInfoLineAsync(
$"✅ Switched to {this._executionModeName} mode.",
ModeColors.Get(this._executionModeName, this._modeColors)).ConfigureAwait(false);
@@ -85,7 +85,6 @@ AIAgent agent =
MaxOutputTokens = MaxOutputTokens,
Name = "ResearchAgent",
Description = "A research assistant that plans and executes research tasks.",
DisableFileAccess = true, // If enabled, this would allow the agent to read/write files in a working directory
OpenTelemetrySourceName = TracingSourceName, // Use our custom source name so spans are captured by the TracerProvider above.
FileMemoryStore = new FileSystemAgentFileStore( // Configure the file memory provider to store files in a local folder called "agent-files".
Path.Combine(AppContext.BaseDirectory, "agent-files")),
@@ -1,6 +1,6 @@
# What this sample demonstrates
This sample demonstrates how to use a `HarnessAgent` with the Harness `AIContextProviders` (`TodoProvider` and `AgentModeProvider`) for interactive research tasks with web search capabilities powered by Azure AI Foundry. The `HarnessAgent` pre-configures function invocation, per-service-call chat history persistence, and context-window compaction.
This sample demonstrates how to use a `HarnessAgent` with the Harness `AIContextProviders` (`TodoProvider` and `AgentModeProvider`) for interactive research tasks with web search capabilities powered by Microsoft Foundry. The `HarnessAgent` pre-configures function invocation, per-service-call chat history persistence, and context-window compaction.
Key features showcased:
@@ -19,7 +19,7 @@ Key features showcased:
Before running this sample, ensure you have:
1. An Azure AI Foundry project with a deployed model (e.g., `gpt-5.4`)
1. A Microsoft Foundry project with a deployed model (e.g., `gpt-5.4`)
2. Azure CLI installed and authenticated (`az login`)
## Environment Variables
@@ -27,7 +27,7 @@ Before running this sample, ensure you have:
Set the following environment variables:
```bash
# Required: Your Azure AI Foundry OpenAI endpoint
# Required: Your Microsoft Foundry OpenAI endpoint
export AZURE_FOUNDRY_OPENAI_ENDPOINT="https://your-project.services.ai.azure.com/openai/v1/"
# Optional: Model deployment name (defaults to gpt-5.4)
@@ -57,7 +57,6 @@ AIAgent webSearchAgent =
DisableTodoProvider = true,
DisableAgentModeProvider = true,
DisableFileMemory = true, // If enabled, this would allow the agent to store memories as files in a directory associated with the current session
DisableFileAccess = true, // If enabled, this would allow the agent to read/write files in a working directory
DisableToolAutoApproval = true, // If true, this disables the don't-ask-again approval functionality.
ChatOptions = new ChatOptions
{
@@ -107,7 +106,6 @@ AIAgent parentAgent =
DisableTodoProvider = true,
DisableAgentModeProvider = true,
DisableFileMemory = true, // If enabled, this would allow the agent to store memories as files in a directory associated with the current session
DisableFileAccess = true, // If enabled, this would allow the agent to read/write files in a working directory
DisableToolAutoApproval = true, // If true, this disables the don't-ask-again approval functionality.
DisableWebSearch = true,
BackgroundAgents = [webSearchAgent],
@@ -32,7 +32,7 @@ A parent agent receives a list of stock tickers and uses a web-search background
## Prerequisites
- An Azure AI Foundry endpoint with an OpenAI model deployment
- A Microsoft Foundry endpoint with an OpenAI model deployment
- Set the following environment variables:
- `AZURE_FOUNDRY_OPENAI_ENDPOINT` — Your Foundry OpenAI endpoint URL
- `FOUNDRY_MODEL` — Model deployment name (defaults to `gpt-5.4`)
@@ -1,12 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use a HarnessAgent with the default FileAccessProvider
// This sample demonstrates how to use a HarnessAgent with the FileAccessProvider
// to give an agent access to a folder of CSV data files. The agent can read, analyze,
// and extract information from the data, then write results back as new files.
//
// The sample includes a pre-populated `working/` folder with sales transaction data.
// The HarnessAgent's default FileAccessProvider uses `{cwd}/working` as its working directory,
// which matches this sample's folder layout.
// File access is opt-in: setting HarnessAgentOptions.FileAccessStore enables the
// FileAccessProvider, and this sample points it at the `working/` folder below the location of the executable.
// Ask the agent to analyze the data, produce summaries, or create new output files.
//
// Special commands:
@@ -1,11 +1,11 @@
# What this sample demonstrates
This sample demonstrates how to use a `HarnessAgent` with the default `FileAccessProvider` to give an agent access to a folder of data files for reading, analyzing, and writing results. The `HarnessAgent` pre-configures function invocation, per-service-call chat history persistence, in-loop compaction, tool approval, and OpenTelemetry — so the sample only needs to supply the chat client, token limits, custom instructions, and opt out of unused features.
This sample demonstrates how to use a `HarnessAgent` with the `FileAccessProvider` to give an agent access to a folder of data files for reading, analyzing, and writing results. The `HarnessAgent` pre-configures function invocation, per-service-call chat history persistence, in-loop compaction, tool approval, and OpenTelemetry — so the sample only needs to supply the chat client, token limits, custom instructions, a `FileAccessStore`, and opt out of unused features.
Key features showcased:
- **HarnessAgent** — a pre-configured agent that wraps a `ChatClientAgent` with function invocation, per-service-call persistence, and context-window compaction
- **FileAccessProvider**the HarnessAgent's default file access provider uses `{cwd}/working` as its working directory, matching this sample's `working/` folder
- **FileAccessProvider**file access is opt-in; setting `HarnessAgentOptions.FileAccessStore` to the sample's `working/` folder enables the provider's read/write tools
- **CSV data processing** — the agent reads sales transaction data and performs analysis on demand
- **Output file creation** — the agent can write summaries, filtered data, or reports back to the data folder
- **Streaming output** — responses are streamed token-by-token for a natural experience
@@ -15,7 +15,7 @@ Key features showcased:
Before running this sample, ensure you have:
1. An Azure AI Foundry project with a deployed model (e.g., `gpt-5.4`)
1. A Microsoft Foundry project with a deployed model (e.g., `gpt-5.4`)
2. Azure CLI installed and authenticated (`az login`)
## Environment Variables
@@ -23,7 +23,7 @@ Before running this sample, ensure you have:
Set the following environment variables:
```bash
# Required: Your Azure AI Foundry OpenAI endpoint
# Required: Your Microsoft Foundry OpenAI endpoint
export AZURE_FOUNDRY_OPENAI_ENDPOINT="https://your-project.services.ai.azure.com/openai/v1/"
# Optional: Model deployment name (defaults to gpt-5.4)
@@ -51,6 +51,15 @@ You can ask the agent to:
E.g. try the following prompt `Please process the sales.csv file by first filtering it to only North region sales, and then calculating the sum of sales by person. I'd like to write the results of the processing to north_region_totals.csv`.
## ⚠️ Security: avoid tool-name collisions
This sample uses `FileAccessProvider.ReadOnlyToolsAutoApprovalRule` to auto-approve read-only file
access tools. Built-in auto-approval rules match tool calls **solely by tool name**, so any other
registered tool that shares one of the approved names (`file_access_read`, `file_access_ls`,
`file_access_grep`) would be **silently auto-approved**, bypassing the
human approval boundary. Ensure no other tool's name collides with the reserved names an
auto-approval rule approves.
## Sample Data
The included `working/sales.csv` contains sales transactions from January to March 2025 with the following columns:
@@ -82,8 +82,9 @@ var instructions =
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
// Create the agent with ALL HarnessAgent features enabled plus Hyperlight CodeAct.
// No Disable* flags are set — TodoProvider, AgentModeProvider, FileMemory, FileAccess,
// ToolApproval, WebSearch, and AgentSkillsProvider are all active.
// TodoProvider, AgentModeProvider, FileMemory, ToolApproval, WebSearch, and
// AgentSkillsProvider are on by default. File access is opt-in, so it is enabled here by
// supplying a FileAccessStore.
AIAgent agent =
new AIProjectClient(
new Uri(endpoint),
@@ -101,6 +102,8 @@ AIAgent agent =
OpenTelemetrySourceName = TracingSourceName,
// Point the file memory at a local folder for persistent memory across sessions.
FileMemoryStore = new FileSystemAgentFileStore(Path.Combine(AppContext.BaseDirectory, "agent-files")),
// Enable file access (opt-in) by rooting the file access tools at a local working folder.
FileAccessStore = new FileSystemAgentFileStore(Path.Combine(AppContext.BaseDirectory, "working")),
// Add the HyperlightCodeActProvider so the agent can execute Python code in a sandbox.
AIContextProviders = [codeAct],
ChatOptions = new ChatOptions
@@ -10,14 +10,14 @@ The agent can plan tasks, manage modes, store memories, read/write files, search
## Prerequisites
- .NET 10 SDK
- An Azure AI Foundry project endpoint
- A Microsoft Foundry project endpoint
- KVM-capable host (the Hyperlight sandbox runs code in micro-VMs)
## Environment Variables
| Variable | Description |
|----------|-------------|
| `FOUNDRY_PROJECT_ENDPOINT` | Your Azure AI Foundry project endpoint |
| `FOUNDRY_PROJECT_ENDPOINT` | Your Microsoft Foundry project endpoint |
| `FOUNDRY_MODEL` | Model deployment name (default: `gpt-5.4`) |
## Running
@@ -174,9 +174,9 @@ async Task ApprovalLoopAsync()
{
AutoApprovalRules =
[
functionCall =>
context =>
{
Console.WriteLine($" Auto-approving: {functionCall.Name}");
Console.WriteLine($" Auto-approving: {context.FunctionCallContent.Name}");
return ValueTask.FromResult(true);
},
],
@@ -252,7 +252,6 @@ AIAgent CreateLeanHarnessAgent(
DisableAgentModeProvider = true,
DisableTodoProvider = disableTodoProvider,
DisableFileMemory = true,
DisableFileAccess = true,
DisableWebSearch = true,
ToolApprovalAgentOptions = toolApprovalAgentOptions,
ChatOptions = new ChatOptions
@@ -32,7 +32,7 @@ The Python sample in [microsoft/agent-framework#6174](https://github.com/microso
Before running this sample, ensure you have:
1. An Azure AI Foundry project with a deployed model (e.g., `gpt-5.4`)
1. A Microsoft Foundry project with a deployed model (e.g., `gpt-5.4`)
2. Azure CLI installed and authenticated (`az login`)
## Environment Variables
@@ -40,7 +40,7 @@ Before running this sample, ensure you have:
Set the following environment variables:
```bash
# Required: Your Azure AI Foundry project endpoint
# Required: Your Microsoft Foundry project endpoint
export AZURE_AI_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/api/projects/your-project"
# Optional: Model deployment name (defaults to gpt-5.4)
@@ -31,7 +31,7 @@ public static class Program
{
private static async Task Main()
{
// Set up the Azure AI Foundry client
// Set up the Microsoft Foundry client
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";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
@@ -92,7 +92,7 @@ public static class Program
string model)
{
ProjectsAgentVersion agentVersion = await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync(
$"{targetLanguage} Translator",
$"{targetLanguage}Translator",
new ProjectsAgentVersionCreationOptions(
new DeclarativeAgentDefinition(model: model)
{
@@ -37,7 +37,7 @@ public static class Program
{
private static async Task Main()
{
// Set up the Azure AI Foundry client
// Set up the Microsoft Foundry client
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";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
@@ -34,7 +34,7 @@ public static class Program
{
private static async Task Main()
{
// Set up the Azure AI Foundry client
// Set up the Microsoft Foundry client
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";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
@@ -35,7 +35,7 @@ public static class Program
{
private static async Task Main()
{
// Set up the Azure AI Foundry client
// Set up the Microsoft Foundry client
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";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
@@ -37,7 +37,7 @@ public static class Program
private static async Task Main()
{
// Set up the Azure AI Foundry client
// Set up the Microsoft Foundry client
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";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
@@ -70,7 +70,7 @@ public static class Program
using var traceProvider = traceProviderBuilder.Build();
// Set up the Azure AI Foundry client
// Set up the Microsoft Foundry client
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";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
@@ -18,7 +18,7 @@ namespace WorkflowMagenticOrchestrationSample;
/// </summary>
/// <remarks>
/// Pre-requisites:
/// - An Azure AI Foundry project endpoint and model deployment must be configured.
/// - A Microsoft Foundry project endpoint and model deployment must be configured.
/// - Run <c>az login</c> before executing the sample.
/// </remarks>
public static class Program
@@ -15,7 +15,7 @@ This sample showcases the Magentic Orchestration Pattern in .NET, setting up a t
## Prerequisites
- `FOUNDRY_PROJECT_ENDPOINT` set to your Azure AI Foundry project endpoint
- `FOUNDRY_PROJECT_ENDPOINT` set to your Microsoft Foundry project endpoint
- `FOUNDRY_MODEL` set to your model deployment name (defaults to `gpt-5.4-mini`)
- `az login` completed before running the sample
@@ -21,13 +21,13 @@ namespace WorkflowAgentsInWorkflowsSample;
/// </summary>
/// <remarks>
/// Pre-requisites:
/// - An Azure AI Foundry project endpoint and model must be configured.
/// - A Microsoft Foundry project endpoint and model must be configured.
/// </remarks>
public static class Program
{
private static async Task Main()
{
// Set up the Azure AI Foundry client
// Set up the Microsoft Foundry client
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";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
@@ -16,13 +16,13 @@ namespace WorkflowAgentsInWorkflowsSample;
/// </summary>
/// <remarks>
/// Pre-requisites:
/// - An Azure AI Foundry project endpoint and model must be configured.
/// - A Microsoft Foundry project endpoint and model must be configured.
/// </remarks>
public static class Program
{
private static async Task Main()
{
// Set up the Azure AI Foundry client.
// Set up the Microsoft Foundry client.
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";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
@@ -31,7 +31,7 @@ namespace MixedWorkflowWithAgentsAndExecutors;
/// <remarks>
/// Pre-requisites:
/// - Previous foundational samples should be completed first.
/// - An Azure AI Foundry project endpoint and model must be configured.
/// - A Microsoft Foundry project endpoint and model must be configured.
/// </remarks>
public static class Program
{
@@ -40,7 +40,7 @@ public static class Program
{
Console.WriteLine("\n=== Mixed Workflow: Agents and Executors ===\n");
// Set up the Azure AI Foundry client
// Set up the Microsoft Foundry client
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";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
@@ -47,7 +47,7 @@ public static class Program
Console.WriteLine("\n=== Writer-Critic Iteration Workflow ===\n");
Console.WriteLine($"Writer and Critic will iterate up to {MaxIterations} times until approval.\n");
// Set up the Azure AI Foundry client
// Set up the Microsoft Foundry client
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";
AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential());
@@ -119,7 +119,7 @@ internal static class Pages
</div>
<div id="chat"></div>
<form id="form">
<input id="input" placeholder="Try: 'What time is it?' or 'Search docs for Azure AI Foundry'" autocomplete="off" autofocus />
<input id="input" placeholder="Try: 'What time is it?' or 'Search docs for Microsoft Foundry'" autocomplete="off" autofocus />
<button type="submit">Send</button>
</form>
<div id="status"></div>
@@ -13,12 +13,12 @@ The WriterAgent is configured with HTTPS redirection so the Aspire DevUI integra
- [.NET 10 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/10.0)
- [Aspire CLI](https://learn.microsoft.com/dotnet/aspire/fundamentals/setup-tooling)
- An Azure subscription with access to [Azure AI Foundry](https://learn.microsoft.com/azure/ai-studio/)
- An Azure subscription with access to [Microsoft Foundry](https://learn.microsoft.com/azure/ai-studio/)
- Azure CLI authenticated (`az login`)
## Azure AI Foundry configuration
## Microsoft Foundry configuration
The sample requires an Azure AI Foundry resource with a deployed `gpt-4.1` model. You have two options:
The sample requires a Microsoft Foundry resource with a deployed `gpt-4.1` model. You have two options:
### Option 1: Connect to an existing Foundry resource
@@ -54,7 +54,7 @@ Remove or comment out the `AsExisting` block in `DevUIIntegration.AppHost/Progra
// foundry.AsExisting(existingFoundryName, existingFoundryResourceGroup);
```
Aspire will provision a new Azure AI Foundry resource on startup. The DevUI resource uses `.WaitFor(foundry)` transitively through the agent services, so the frontend won't become available until provisioning completes. This can take several minutes on first run.
Aspire will provision a new Microsoft Foundry resource on startup. The DevUI resource uses `.WaitFor(foundry)` transitively through the agent services, so the frontend won't become available until provisioning completes. This can take several minutes on first run.
You still need to fill in the `Azure` section of `appsettings.json` (subscription, location, etc.) so Aspire knows where to create the resource.
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample evaluates a pre-existing Azure AI Foundry agent against a rubric evaluator
// This sample evaluates a pre-existing Microsoft Foundry agent against a rubric evaluator
// that was authored in the Foundry portal.
//
// Rubric evaluators are LLM-as-judge evaluators with custom scoring dimensions you define
@@ -9,7 +9,7 @@
// here by name and version.
//
// Prerequisites:
// - An Azure AI Foundry project with a deployed model.
// - A Microsoft Foundry project with a deployed model.
// - A registered Foundry agent in that project (the rubric was created against this agent).
// - A rubric evaluator already created in the Foundry portal.
// - .env (or environment) populated with the FOUNDRY_* variables below.
@@ -1,6 +1,6 @@
# Evaluation — Foundry Rubric
This sample evaluates a pre-existing Azure AI Foundry agent against a **rubric evaluator**
This sample evaluates a pre-existing Microsoft Foundry agent against a **rubric evaluator**
authored in the Foundry portal. Rubric evaluators are LLM-as-judge evaluators with custom
scoring dimensions you define for your domain; agent-framework references them by name and
version, mixes them with built-in evaluators, and exposes per-dimension scores you can gate
@@ -20,7 +20,7 @@ CI on.
- .NET 10 SDK or later.
- Azure CLI installed and authenticated (`az login`).
- An Azure AI Foundry project with a deployed model.
- A Microsoft Foundry project with a deployed model.
- A registered Foundry agent in that project (the agent the rubric was created against).
- A rubric evaluator created in the Foundry portal. Creating rubrics through the portal
currently requires picking a Foundry agent as the generation context, so this
+1 -1
View File
@@ -30,7 +30,7 @@ dotnet/samples/
│ │ └── openai/ # OpenAI provider samples
│ ├── AgentOpenTelemetry/ # OpenTelemetry integration
│ ├── AgentSkills/ # Agent skills patterns
│ ├── AgentWithMemory/ # Memory providers (chat history, Mem0, Foundry)
│ ├── AgentWithMemory/ # Memory providers (chat history, Mem0, Valkey, Foundry, AgentMemory)
│ ├── AgentWithRAG/ # RAG patterns (text, vector store, Foundry)
│ ├── AGUI/ # AG-UI protocol samples
│ ├── DeclarativeAgents/ # Declarative agent definitions
@@ -20,7 +20,7 @@ public static class AnthropicClientExtensions
/// <summary>
/// Creates a new AI agent using the specified model and options.
/// </summary>
/// <param name="client">An Anthropic <see cref="IAnthropicClient"/> to use with the agent..</param>
/// <param name="client">An Anthropic <see cref="IAnthropicClient"/> to use with the agent.</param>
/// <param name="model">The model to use for chat completions.</param>
/// <param name="instructions">The instructions for the AI agent.</param>
/// <param name="name">The name of the AI agent.</param>
@@ -74,7 +74,7 @@ public static class AnthropicClientExtensions
/// <summary>
/// Creates an AI agent from an <see cref="IAnthropicClient"/> using the Anthropic Chat Completion API.
/// </summary>
/// <param name="client">An Anthropic <see cref="IAnthropicClient"/> to use with the agent..</param>
/// <param name="client">An Anthropic <see cref="IAnthropicClient"/> to use with the agent.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="loggerFactory">Optional logger factory for enabling logging within the agent.</param>
@@ -607,7 +607,10 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
[Newtonsoft.Json.JsonProperty("type")]
public string Type { get; set; } = string.Empty;
[Newtonsoft.Json.JsonProperty("ttl")]
// Omit "ttl" from the document when null so Cosmos DB leaves TTL unset (disabled) instead of
// rejecting the write. Cosmos requires ttl to be a positive integer or -1; a literal null is
// invalid, so serializing MessageTtlSeconds = null must drop the property entirely.
[Newtonsoft.Json.JsonProperty("ttl", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public int? Ttl { get; set; }
/// <summary>
@@ -19,7 +19,7 @@ using OpenAI.Evals;
namespace Microsoft.Agents.AI.Foundry;
/// <summary>
/// Azure AI Foundry evaluator provider that calls the Foundry Evals API.
/// Microsoft Foundry evaluator provider that calls the Foundry Evals API.
/// </summary>
/// <remarks>
/// <para>
@@ -28,7 +28,7 @@ namespace Microsoft.Agents.AI.Foundry;
/// (quality, safety, agent behavior, tool usage) are supported.
/// </para>
/// <para>
/// Results appear in the Azure AI Foundry portal with a report URL for detailed analysis.
/// Results appear in the Microsoft Foundry portal with a report URL for detailed analysis.
/// </para>
/// </remarks>
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Serializing Dictionary<string, object> for eval API payloads.")]
@@ -55,7 +55,7 @@ public sealed class FoundryEvals : IAgentEvaluator
/// <summary>
/// Initializes a new instance of the <see cref="FoundryEvals"/> class.
/// </summary>
/// <param name="projectClient">The Azure AI Foundry project client.</param>
/// <param name="projectClient">The Microsoft Foundry project client.</param>
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
/// <param name="evaluators">
/// Evaluator specs to use. Each entry can be a built-in evaluator name (string, for example
@@ -80,7 +80,7 @@ public sealed class FoundryEvals : IAgentEvaluator
/// <summary>
/// Initializes a new instance of the <see cref="FoundryEvals"/> class with a conversation splitter.
/// </summary>
/// <param name="projectClient">The Azure AI Foundry project client.</param>
/// <param name="projectClient">The Microsoft Foundry project client.</param>
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
/// <param name="splitter">
/// Default conversation splitter for multi-turn conversations.
@@ -104,7 +104,7 @@ public sealed class FoundryEvals : IAgentEvaluator
/// <summary>
/// Initializes a new instance of the <see cref="FoundryEvals"/> class with full configuration.
/// </summary>
/// <param name="projectClient">The Azure AI Foundry project client.</param>
/// <param name="projectClient">The Microsoft Foundry project client.</param>
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
/// <param name="splitter">
/// Default conversation splitter for multi-turn conversations.
@@ -141,7 +141,7 @@ public sealed class FoundryEvals : IAgentEvaluator
/// Initializes a new instance of the <see cref="FoundryEvals"/> class using built-in evaluator
/// names. Preserves source compatibility for callers that pass a <see cref="string"/> array.
/// </summary>
/// <param name="projectClient">The Azure AI Foundry project client.</param>
/// <param name="projectClient">The Microsoft Foundry project client.</param>
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
/// <param name="evaluators">Built-in evaluator names (for example <see cref="Relevance"/>).</param>
public FoundryEvals(AIProjectClient projectClient, string model, string[] evaluators)
@@ -153,7 +153,7 @@ public sealed class FoundryEvals : IAgentEvaluator
/// Initializes a new instance of the <see cref="FoundryEvals"/> class with a splitter and
/// built-in evaluator names.
/// </summary>
/// <param name="projectClient">The Azure AI Foundry project client.</param>
/// <param name="projectClient">The Microsoft Foundry project client.</param>
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
/// <param name="splitter">Default conversation splitter for multi-turn conversations.</param>
/// <param name="evaluators">Built-in evaluator names.</param>
@@ -170,7 +170,7 @@ public sealed class FoundryEvals : IAgentEvaluator
/// Initializes a new instance of the <see cref="FoundryEvals"/> class with full configuration
/// and built-in evaluator names.
/// </summary>
/// <param name="projectClient">The Azure AI Foundry project client.</param>
/// <param name="projectClient">The Microsoft Foundry project client.</param>
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
/// <param name="splitter">Default conversation splitter for multi-turn conversations.</param>
/// <param name="pollIntervalSeconds">Seconds between status polls.</param>
@@ -355,7 +355,7 @@ public sealed class FoundryEvals : IAgentEvaluator
/// Source-compat overload of <see cref="EvaluateTracesAsync(AIProjectClient, string, IEnumerable{string}, IEnumerable{string}, string, int, FoundryEvaluatorSpec[], string, double, double, CancellationToken)"/>
/// that accepts a <see cref="string"/> array of built-in evaluator names.
/// </summary>
/// <param name="projectClient">The Azure AI Foundry project client.</param>
/// <param name="projectClient">The Microsoft Foundry project client.</param>
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
/// <param name="responseIds">Evaluate specific Responses API response IDs.</param>
/// <param name="traceIds">Evaluate specific OTel trace IDs from App Insights.</param>
@@ -403,7 +403,7 @@ public sealed class FoundryEvals : IAgentEvaluator
/// <paramref name="lookbackHours"/> to evaluate recent activity.
/// </para>
/// </remarks>
/// <param name="projectClient">The Azure AI Foundry project client.</param>
/// <param name="projectClient">The Microsoft Foundry project client.</param>
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
/// <param name="responseIds">Evaluate specific Responses API response IDs.</param>
/// <param name="traceIds">Evaluate specific OTel trace IDs from App Insights.</param>
@@ -560,7 +560,7 @@ public sealed class FoundryEvals : IAgentEvaluator
/// Source-compat overload of <see cref="EvaluateFoundryTargetAsync(AIProjectClient, string, IDictionary{string, object}, IEnumerable{string}, FoundryEvaluatorSpec[], string, double, double, CancellationToken)"/>
/// that accepts a <see cref="string"/> array of built-in evaluator names.
/// </summary>
/// <param name="projectClient">The Azure AI Foundry project client.</param>
/// <param name="projectClient">The Microsoft Foundry project client.</param>
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
/// <param name="target">Target configuration (must include a "type" key).</param>
/// <param name="testQueries">Queries for Foundry to send to the target.</param>
@@ -598,7 +598,7 @@ public sealed class FoundryEvals : IAgentEvaluator
/// Foundry invokes the target, captures the output, and evaluates it.
/// Use this for scheduled evaluations, red teaming, and CI/CD quality gates.
/// </remarks>
/// <param name="projectClient">The Azure AI Foundry project client.</param>
/// <param name="projectClient">The Microsoft Foundry project client.</param>
/// <param name="model">Model deployment name for the LLM judge evaluator.</param>
/// <param name="target">Target configuration (must include a "type" key, e.g. "azure_ai_agent").</param>
/// <param name="testQueries">Queries for Foundry to send to the target.</param>
@@ -43,13 +43,13 @@ namespace Microsoft.Agents.AI;
/// <item><description><see cref="TodoProvider"/> — persistent todo list that the agent uses to track multi-step plans. Disable with <see cref="HarnessAgentOptions.DisableTodoProvider"/>.</description></item>
/// <item><description><see cref="AgentModeProvider"/> — mode tracking (e.g., "plan" vs "execute") that the agent uses to structure its work. Disable with <see cref="HarnessAgentOptions.DisableAgentModeProvider"/>.</description></item>
/// <item><description><see cref="FileMemoryProvider"/> — file-based session memory allowing the agent to persist notes and artifacts across turns. Disable with <see cref="HarnessAgentOptions.DisableFileMemory"/>.</description></item>
/// <item><description><see cref="FileAccessProvider"/> — shared file access providing read/write tools for a working directory. Disable with <see cref="HarnessAgentOptions.DisableFileAccess"/>.</description></item>
/// <item><description><see cref="AgentSkillsProvider"/> — discovers and loads skill definitions from the file system, enabling dynamic tool sets. Disable with <see cref="HarnessAgentOptions.DisableAgentSkillsProvider"/>.</description></item>
/// </list>
/// </para>
/// <para>
/// <strong>Optional context providers (enabled via <see cref="HarnessAgentOptions"/>):</strong>
/// <list type="bullet">
/// <item><description><see cref="FileAccessProvider"/> — shared file access providing read/write tools for a working directory. Enable by setting <see cref="HarnessAgentOptions.FileAccessStore"/>; configure via <see cref="HarnessAgentOptions.FileAccessProviderOptions"/>.</description></item>
/// <item><description><see cref="BackgroundAgentsProvider"/> — enables delegation to background agents for parallel work. Enable by setting <see cref="HarnessAgentOptions.BackgroundAgents"/>.</description></item>
/// <item><description><c>ShellEnvironmentProvider</c> — injects OS/shell/CWD information and a shell execution tool. Enable by setting <c>HarnessAgentOptions.ShellExecutor</c> (.NET only).</description></item>
/// </list>
@@ -320,13 +320,9 @@ public sealed class HarnessAgent : DelegatingAIAgent
}));
}
if (options?.DisableFileAccess is not true)
if (options?.FileAccessStore is AgentFileStore fileAccessStore)
{
AgentFileStore fileAccessStore = options?.FileAccessStore
?? new FileSystemAgentFileStore(
Path.Combine(Directory.GetCurrentDirectory(), "working"));
providers.Add(new FileAccessProvider(fileAccessStore));
providers.Add(new FileAccessProvider(fileAccessStore, options.FileAccessProviderOptions));
}
if (options?.DisableAgentSkillsProvider is not true)
@@ -237,25 +237,25 @@ public sealed class HarnessAgentOptions
public AgentFileStore? FileMemoryStore { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the <see cref="FileAccessProvider"/> is disabled.
/// Gets or sets the <see cref="AgentFileStore"/> that enables the <see cref="FileAccessProvider"/>.
/// </summary>
/// <remarks>
/// When <see langword="false"/> (the default), a <see cref="FileAccessProvider"/> is included in the
/// agent's context providers, using either <see cref="FileAccessStore"/> or a default
/// <see cref="FileSystemAgentFileStore"/> rooted at <c>{cwd}/working</c>.
/// </remarks>
public bool DisableFileAccess { get; set; }
/// <summary>
/// Gets or sets a custom <see cref="AgentFileStore"/> for the <see cref="FileAccessProvider"/>.
/// </summary>
/// <remarks>
/// When <see langword="null"/> and <see cref="DisableFileAccess"/> is <see langword="false"/>,
/// a default <see cref="FileSystemAgentFileStore"/> is created.
/// This property is ignored when <see cref="DisableFileAccess"/> is <see langword="true"/>.
/// File access is opt-in. When <see langword="null"/> (the default), no <see cref="FileAccessProvider"/>
/// is added and the agent has no file access tools. When set, a <see cref="FileAccessProvider"/> is
/// included in the agent's context providers, backed by the supplied store and configured with
/// <see cref="FileAccessProviderOptions"/> when provided.
/// </remarks>
public AgentFileStore? FileAccessStore { get; set; }
/// <summary>
/// Gets or sets the <see cref="FileAccessProviderOptions"/> used to configure the <see cref="FileAccessProvider"/>.
/// </summary>
/// <remarks>
/// This property is only used when <see cref="FileAccessStore"/> is set (file access is opt-in).
/// When <see langword="null"/>, the provider uses its default options.
/// </remarks>
public FileAccessProviderOptions? FileAccessProviderOptions { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the <see cref="HostedWebSearchTool"/> is disabled.
/// </summary>
@@ -375,8 +375,17 @@ public sealed class HarnessAgentOptions
/// Gets or sets the name of the shell execution tool exposed to the model.
/// </summary>
/// <remarks>
/// <para>
/// When <see langword="null"/> (the default), the shell executor's default tool name (<c>run_shell</c>) is used.
/// This property is ignored when <see cref="ShellExecutor"/> is <see langword="null"/>.
/// </para>
/// <para>
/// <b>Security warning:</b> auto-approval rules may match tool calls solely by name. Pay attention to
/// the tool names approved by auto-approval rules for other features. Setting this property to a
/// value that collides with a tool name that is approved by an auto-approval rule for another feature will cause
/// the shell tool to also be auto-approved, bypassing the human approval boundary. Choose a unique
/// name that no other registered tool uses.
/// </para>
/// </remarks>
public string? ShellToolName { get; set; }
@@ -271,6 +271,7 @@ class _CodeValidator(ast.NodeVisitor):
self._allowed_builtins = allowed_builtins if allowed_builtins is not None else ALLOWED_BUILTINS
self._blocked_builtins = blocked_builtins if blocked_builtins is not None else BLOCKED_BUILTINS
self._allowed_os_attrs = allowed_os_attrs if allowed_os_attrs is not None else ALLOWED_OS_ATTRS
self._os_aliases: set[str] = {"os"}
def validate(self, code: str) -> None:
"""Validate code and raise CodeValidationError if it violates policy."""
@@ -280,6 +281,7 @@ class _CodeValidator(ast.NodeVisitor):
raise CodeValidationError(f"Syntax error in generated code: {exc}") from exc
self._errors = []
self._os_aliases = {"os"}
self.visit(tree)
if self._errors:
@@ -303,6 +305,10 @@ class _CodeValidator(ast.NodeVisitor):
self._errors.append(f"Import of '{alias_node.name}' is not allowed (blocked: {module_name})")
elif module_name not in self._allowed_imports:
self._errors.append(f"Import of '{alias_node.name}' is not allowed (not in allow-list)")
if alias_node.name == "os":
self._os_aliases.add(alias_node.asname or "os")
elif alias_node.name.startswith("os.") and alias_node.asname is None:
self._os_aliases.add("os")
self.generic_visit(node)
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
@@ -324,6 +330,32 @@ class _CodeValidator(ast.NodeVisitor):
self._errors.append(f"Import from 'os' of '{alias_node.name}' is not allowed")
self.generic_visit(node)
def visit_Assign(self, node: ast.Assign) -> None:
"""Track re-bindings of the ``os`` module."""
for target in node.targets:
self._track_os_alias_targets(target, node.value)
self.generic_visit(node)
def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
"""Track annotated re-bindings of the ``os`` module."""
if (
isinstance(node.value, ast.Name)
and node.value.id in self._os_aliases
and isinstance(node.target, ast.Name)
):
self._os_aliases.add(node.target.id)
self.generic_visit(node)
def _track_os_alias_targets(self, target: ast.AST, value: ast.AST) -> None:
if isinstance(target, ast.Starred):
target = target.value
if isinstance(target, ast.Name) and isinstance(value, ast.Name) and value.id in self._os_aliases:
self._os_aliases.add(target.id)
elif isinstance(target, (ast.Tuple, ast.List)) and isinstance(value, (ast.Tuple, ast.List)):
for target_item, value_item in zip(target.elts, value.elts):
self._track_os_alias_targets(target_item, value_item)
def visit_Call(self, node: ast.Call) -> None:
"""Validate function calls.
@@ -357,7 +389,7 @@ class _CodeValidator(ast.NodeVisitor):
# Enforce the `os` attribute allow-list. Anything outside `ALLOWED_OS_ATTRS`
# (file I/O, process control, mutating helpers, etc.) is rejected so the
# validator matches the documented `os.environ` / `os.path`-only contract.
if isinstance(node.value, ast.Name) and node.value.id == "os" and node.attr not in self._allowed_os_attrs:
if isinstance(node.value, ast.Name) and node.value.id in self._os_aliases and node.attr not in self._allowed_os_attrs:
self._errors.append(f"Access to os.{node.attr} is not allowed")
# Block access to certain dangerous attributes
@@ -17,7 +17,7 @@ internal interface IScopedContentProcessor
/// Process a list of messages.
/// The list of messages should be a prompt or response.
/// </summary>
/// <param name="messages">A list of <see cref="ChatMessage"/> objects sent to the agent or received from the agent..</param>
/// <param name="messages">A list of <see cref="ChatMessage"/> objects sent to the agent or received from the agent.</param>
/// <param name="sessionId">The session where the messages were sent.</param>
/// <param name="activity">An activity to indicate prompt or response.</param>
/// <param name="purviewSettings">Purview settings containing tenant id, app name, etc.</param>
@@ -255,6 +255,14 @@ public sealed class DockerShellExecutor : ShellExecutor
/// gating. Container configuration alone is not a sufficient signal
/// to safely auto-execute model-generated commands — the
/// approval/policy decision belongs to the agent author.
/// <para>
/// <b>Security warning:</b> auto-approval rules may match tool calls solely by name. Pay attention to
/// the tool names approved by auto-approval rules for other features. Setting <paramref name="name"/>
/// to a value that collides with a tool name that is approved by an auto-approval rule for another feature will
/// cause this shell tool to also be auto-approved even when <paramref name="requireApproval"/> is
/// <see langword="true"/>, bypassing the human approval boundary. Choose a unique name that no other
/// registered tool uses.
/// </para>
/// </remarks>
/// <param name="name">Function name surfaced to the model.</param>
/// <param name="description">Function description for the model.</param>
@@ -21,9 +21,10 @@ namespace Microsoft.Agents.AI.Tools.Shell;
/// <para>
/// The buffer counts UTF-8 bytes (matching the public <c>maxOutputBytes</c> contract
/// and <see cref="ShellSession.TruncateHeadTail"/>). Append happens one rune at a time
/// — when the head fills, the next rune's UTF-8 bytes go to the tail as an indivisible
/// unit, and the oldest rune is dropped from the tail. This guarantees the final
/// string never contains a split rune (no orphan surrogates, no invalid UTF-8).
/// — once a complete rune no longer fits in the head, it and all later runes go to
/// the tail as indivisible units. After the total exceeds the cap, the oldest tail
/// runes are dropped. This guarantees the final string never contains a split rune
/// (no orphan surrogates, no invalid UTF-8).
/// </para>
/// </remarks>
internal sealed class HeadTailBuffer
@@ -37,6 +38,7 @@ internal sealed class HeadTailBuffer
private readonly Queue<byte[]> _tail = new();
private int _tailBytes;
private long _totalBytes;
private bool _headSealed;
public HeadTailBuffer(int cap)
{
@@ -63,19 +65,22 @@ internal sealed class HeadTailBuffer
var n = rune.EncodeToUtf8(scratch);
this._totalBytes += n;
if (this._head.Count + n <= this._headCap)
if (!this._headSealed && this._head.Count + n <= this._headCap)
{
for (var i = 0; i < n; i++) { this._head.Add(scratch[i]); }
continue;
}
// Head is full — append to tail as a single rune-sized chunk.
// Once a complete rune cannot fit in the head, seal it and keep all later runes in the tail.
this._headSealed = true;
var bytes = scratch[..n].ToArray();
this._tail.Enqueue(bytes);
this._tailBytes += n;
// Evict whole runes from the front of the tail until we fit.
while (this._tailBytes > this._tailCap && this._tail.Count > 0)
while (this._totalBytes > this._cap &&
this._tailBytes > this._tailCap &&
this._tail.Count > 0)
{
var dropped = this._tail.Dequeue();
this._tailBytes -= dropped.Length;
@@ -325,6 +325,14 @@ public sealed class LocalShellExecutor : ShellExecutor
/// container where the tool itself is the boundary).
/// </param>
/// <returns>An <see cref="AIFunction"/> wrapping <see cref="RunAsync"/>.</returns>
/// <remarks>
/// <b>Security warning:</b> auto-approval rules may match tool calls solely by name. Pay attention to
/// the tool names approved by auto-approval rules for other features. Setting <paramref name="name"/>
/// to a value that collides with a tool name that is approved by an auto-approval rule for another feature will
/// cause this shell tool to also be auto-approved even when <paramref name="requireApproval"/> is
/// <see langword="true"/>, bypassing the human approval boundary. Choose a unique name that no other
/// registered tool uses.
/// </remarks>
public override AIFunction AsAIFunction(string name = "run_shell", string? description = null, bool requireApproval = true)
{
if (!requireApproval && !this._acknowledgeUnsafe)
@@ -78,6 +78,13 @@ public abstract class ShellExecutor : IAsyncDisposable
/// explicit user approval before executing.
/// </param>
/// <returns>An <see cref="AIFunction"/> wrapping <see cref="RunAsync"/>.</returns>
/// <remarks>
/// <b>Security warning:</b> auto-approval rules may match tool calls solely by name. Pay attention to
/// the tool names approved by auto-approval rules for other features. Setting <paramref name="name"/>
/// to a value that collides with a tool name that is approved by an auto-approval rule for another feature will
/// cause this shell tool to also be auto-approved, bypassing the human approval boundary. Choose a
/// unique name that no other registered tool uses.
/// </remarks>
public abstract AIFunction AsAIFunction(string name = "run_shell", string? description = null, bool requireApproval = true);
/// <inheritdoc />
@@ -13,10 +13,6 @@
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<PropertyGroup>
<!-- Disable packing until we are ready to release this as a nuget -->
<IsPackable>false</IsPackable>
</PropertyGroup>
<PropertyGroup>
<!-- NuGet Package Settings -->
@@ -42,6 +42,28 @@ internal static class JsonDocumentExtensions
};
}
/// <summary>
/// Maps a successfully-parsed JSON document's root element to a CLR value by its <see cref="JsonValueKind"/>.
/// Mirrors the value-kind handling shared by the agent/tool/HTTP executors: objects become records,
/// arrays become lists, and scalars become their primitive value.
/// </summary>
/// <param name="jsonDocument">The parsed JSON document.</param>
/// <param name="rawJson">The original JSON text, returned as a fallback when the root kind is undefined.</param>
/// <returns>The parsed CLR value.</returns>
public static object? ParseJsonValue(this JsonDocument jsonDocument, string rawJson) =>
jsonDocument.RootElement.ValueKind switch
{
JsonValueKind.Object => jsonDocument.ParseRecord(VariableType.RecordType),
JsonValueKind.Array when jsonDocument.RootElement.GetArrayLength() == 0 => new List<object?>(),
JsonValueKind.Array => jsonDocument.ParseList(jsonDocument.RootElement.GetListTypeFromJson()),
JsonValueKind.String => jsonDocument.RootElement.GetString(),
JsonValueKind.Number => jsonDocument.RootElement.GetDouble(),
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null => null,
_ => rawJson,
};
/// <summary>
/// Creates a VariableType.List with schema inferred from the first object element in the array.
/// </summary>

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