Compare commits

...

87 Commits

Author SHA1 Message Date
Javier Calvarro Nelson e556c81924 Fix AGUI overload tests after rebase
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-18 18:49:00 +02:00
Javier Calvarro Nelson 18d05e8824 Add MapAGUI hosting overloads with IHostedAgentBuilder and agent name support
This adds the same hosting patterns from A2A and OpenAI to AGUI:
- MapAGUI(IHostedAgentBuilder) and MapAGUI(IHostedAgentBuilder, string? path)
- MapAGUI(string agentName) and MapAGUI(string agentName, string? path)
- MapAGUI(AIAgent) and MapAGUI(AIAgent, string? path)
- ValidateAgentName for URL-safe validation
- Updated namespace to Microsoft.AspNetCore.Builder
- Renamed class to MicrosoftAgentAIHostingAGUIEndpointRouteBuilderExtensions
- Added comprehensive unit tests
2026-08-18 18:35:45 +02:00
SergeyMenshykh 4ce2804db0 .NET: Fix release build analyzer failures (#7721)
Guard the hosted storage error log before evaluating the agent name and update the SDK to the servicing release containing the net9 ILLink analyzer fix.

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

Copilot-Session: e3a6cce8-e1e8-4cf2-9cf4-3c0c8f3ed6d8
2026-08-18 13:17:22 +00:00
SergeyMenshykh 1b45c15749 .NET: Update version for 1.18.0 release (#7713)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0acc90aa-5690-41e6-b546-16083fc1c8b5
2026-08-18 10:01:07 +00:00
badhope 00d7102c54 Python: fix(workflows): preserve all trace contexts in FanInEdgeRunner aggregation (#7557)
* fix(workflows): preserve all trace contexts in FanInEdgeRunner aggregation

FanInEdgeRunner collected trace contexts and source span IDs using the
singular backward-compat properties (msg.trace_context / msg.source_span_id),
which return only the first element of the plural lists. When a message
arriving at a fan-in already carries multiple trace contexts (e.g. from
a prior fan-in aggregation), all but the first were silently dropped.

Iterate over the plural fields (trace_contexts / source_span_ids) and
extend the aggregated lists so every trace context and source span ID
from every source message is preserved. This keeps distributed tracing
links intact for nested fan-in topologies.

Added test_fan_in_preserves_multiple_trace_contexts_per_message that
sends a message with two trace contexts through a fan-in and asserts
all three contexts (2 + 1) reach the target executor.

* fix: address Copilot review comments on trace context aggregation

1. Pair trace_contexts and source_span_ids per-message (via zip) instead
   of flattening independently. This prevents misalignment when a message
   has mismatched counts — orphans are dropped per-message rather than
   shifting all subsequent pairs out of alignment.

2. Remove TraceCapturingAggregator's override of Executor.execute()
   (documented as "do not override"). Capture trace data from the
   WorkflowContext passed to the handler instead.

---------

Co-authored-by: weed33834 <weed33834@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-08-18 04:26:04 +00:00
Evan Mattson d80c340a06 Clarify function-loop spec update guidance (#7706) 2026-08-18 02:11:10 +00:00
Evan Mattson af4347a61d Python: Restrict workflow type deserialization (#7500)
Resolve request-info type names only from exact caller-provided mappings or already-loaded module namespaces. Remove payload-selected imports and add focused regression coverage for both request and response type fields.

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

Copilot-Session: a53fe20b-c3f0-4583-badc-d5deac7c1049
2026-08-18 02:08:21 +00:00
dependabot[bot] a445e4815d Bump ty from 0.0.64 to 0.0.70 in /python (#7644)
Bumps [ty](https://github.com/astral-sh/ty) from 0.0.64 to 0.0.70.
- [Release notes](https://github.com/astral-sh/ty/releases)
- [Changelog](https://github.com/astral-sh/ty/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ty/compare/0.0.64...0.0.70)

---
updated-dependencies:
- dependency-name: ty
  dependency-version: 0.0.69
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 23:30:52 +00:00
dependabot[bot] 8b8fbbba03 Bump flit from 3.12.0 to 4.0.2 in /python (#7645)
Bumps [flit](https://github.com/pypa/flit) from 3.12.0 to 4.0.2.
- [Changelog](https://github.com/pypa/flit/blob/main/doc/history.rst)
- [Commits](https://github.com/pypa/flit/compare/3.12.0...4.0.2)

---
updated-dependencies:
- dependency-name: flit
  dependency-version: 4.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-17 23:30:33 +00:00
Ruiming Zhao 925d722acf Python: clarify skill script argument guidance (#7695)
* Python: clarify skill script argument guidance

* test: harden skill argument guidance coverage
2026-08-17 21:38:58 +00:00
Peter Ibekwe 6001c12cd3 .NET: Fix declarative workflows deep research sample (#7674)
* Fix declarative workflows deep research sample

* Address PR comments
2026-08-17 19:50:16 +00:00
Tao Chen 6a3633e54a Python: Add a global workflow checkpoint type registry (#7636)
* Add a glocal checkpoint type registry

* Update samples

* Revert uv.lock

* Address comments

* Revert uv.lock

* Revert uv.lock
2026-08-17 18:33:01 +00:00
LeoZhaoo 648a31ade6 Python: Surface A2A preview consent URLs (#7606)
* fix(foundry-hosting): surface A2A consent URLs

* Use non-hashing membership

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

---------

Co-authored-by: Tao Chen <williamchan444307762@hotmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Tao Chen <taochen@microsoft.com>
2026-08-17 18:31:02 +00:00
Ilia Sokolov c6584ffaad .NET: Add Cosmos chat history retrieval API (#7412)
* .NET: Add Cosmos chat history retrieval API

* Clarify Cosmos message ordering semantics
2026-08-17 17:45:02 +00:00
Roger Barreto 74808cb6c7 .NET: Add Foundry hosted session and user identity pass-through (#7648)
* .NET: Add Foundry session and user identity pass-through

Let user agents pin hosted agent_session_id on AgentSession and
send x-ms-user-identity per call for Foundry hosted agents.

* .NET: Add live ITs for Foundry session and user identity

Cover service-managed and admin-pinned hosted sandboxes, sticky
hosted session id, and per-call x-ms-user-identity isolation with
separate AgentSessions sharing one sandbox. Echo container avoids
model quota for identity assertions.

* .NET: Reject Foundry hosted session switch when sticky

Persist sticky id in finally, clone run options before factory wrap,
validate whitespace pin on CreateHostedSessionAsync, and throw on
unexpected hosted session id change in the response. Docs: distinct
AgentSessions per user identity may share one sandbox.

* .NET: Clear nested user identity and preserve run options

Always assign UserIdentityScope including null so nested runs do not
inherit a parent identity. When upgrading plain AgentRunOptions, keep
background, format, and additional properties on the specialized clone.

* .NET: Clarify previous_response_id user binding in docs

Align WithUserIdentity guidance with Foundry Learn multiplex docs:
response chains are bound to the creating user even inside a shared
hosted sandbox.

* refactor(foundry): clarify hosted agent APIs
2026-08-17 17:28:31 +00:00
Roger Barreto 11592495db docs: fix Agent Lightning installation link (#7693) 2026-08-17 16:49:12 +00:00
ump45nose 047ec7eaff .NET: Allow agents to opt into concurrent tool invocation (#7650)
* .NET: allow agents to opt into concurrent tool invocation

* .NET: address concurrent invocation review feedback
2026-08-17 11:15:27 +00:00
Chinedum Echeta 9c3a1a4af7 Python: Enhance _OutputItemTracker to prevent duplicate function call streaming (#7486)
* Python: Enhance _OutputItemTracker to prevent duplicate function call streaming

* Handle empty function call metadata arguments

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

Copilot-Session: 9b232bff-6a8c-4b02-addd-89de57a82f6a

* Python: Refactor _OutputItemTracker to manage outstanding function calls and update tests for call ID reuse

---------

Copilot-Session: 9b232bff-6a8c-4b02-addd-89de57a82f6a
2026-08-16 16:22:30 +00:00
Atharva Vichare 228754d7fa Python: Fix AG-UI url source dropping attachments when the URL is in source.value (#7655)
The ag-ui-protocol `InputContentUrlSource` carries the URL in `source.value`,
but `_extract_multimodal_source_fields` only read `source.url`/`source.uri`
for url-typed sources, so attachments sent in the spec shape were dropped
during the AG-UI to MAF conversion. The base64 branch already read
`source.value` correctly.

Read `source.value` first, keeping `url`/`uri` as fallbacks for the non-spec
shape. Adds tests for both.

Fixes #7653
2026-08-16 16:12:17 +00:00
Chinedum Echeta 8461667fe4 fix: deduplicate streamed DevUI tool calls (#7652)
Refs #7651

🐛 - Generated by Copilot
2026-08-16 16:11:28 +00:00
Roger Barreto 12621e0a74 .NET: Fix IDE0039 by using local functions in samples (#7666)
Replace Func lambda assignments with local functions so
dotnet format --verify-no-changes passes on the agent and RAG samples.
2026-08-14 15:32:54 +00:00
Tao Chen e1e005f226 Enforce code owner (#7660)
* Draft: Enforce code owner

* Apply new assignments after feedback

* Address comments
2026-08-14 15:16:53 +00:00
westey e289320027 Python: Add approval storage and improve truth checks (#7631)
* Add approval storage and improve truth checks

* Address PR comments

* Update spec

* Revert changes to agui since it is already handled in another pr

* Add missed change
2026-08-14 09:52:47 +00:00
Evan Mattson ae7fa3389c Python: Bump Python package versions for 1.14.0 release (#7661)
* Bump Python package versions for 1.14.0 release

Bump the CHANGELOG-selected packages for the 1.14.0 release: minor versions for root/core, AG-UI, Foundry, OpenAI, and orchestrations due to additive public APIs; patch versions for declarative and GitHub Copilot fixes; and Pacific-date prerelease stamps only for changed alpha/beta packages. No beta cohort bump was applied. Core dependency floors follow the strict policy and remain unchanged because no dependent package requires a new 1.14 API. Release validation also identified and corrected missing AG-UI and Copilot Studio runtime dependencies and aligned GitHub Copilot metadata with its Python 3.11 SDK requirement. Lab is intentionally skipped because its changes are development-only, and the moved Azure Functions and Durable Task packages are documented but no longer versioned here.

* Raise AG-UI core dependency floor
2026-08-14 11:06:35 +09:00
Evan Mattson 4aa737eee5 Python: [BREAKING] Require building functional workflow instances (#7521)
* Harden functional workflow continuation authority

Use a versioned opaque single-use token on WorkflowRunResult, validate it before request correlation, consume it immediately before replayed user code, and rotate it on each pause. Carry the same explicit authority through streaming and non-streaming FunctionalWorkflowAgent responses.

Files changed: functional workflow/runtime result APIs, functional HITL regression tests, core agent guidance, and the functional HITL sample.

Next iteration: enforce pending-state overlap and token-authorized abandonment, then document and test checkpoint authorization boundaries.

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

* Enforce one pending functional continuation

Reject fresh messages and checkpoint restores while an in-memory continuation is pending. Add token-authorized abandonment on FunctionalWorkflow and FunctionalWorkflowAgent, and clear retained replay state atomically when authority is consumed while preserving the active message for token rotation and checkpoints.

Files changed: functional workflow runtime and agent adapter, functional lifecycle regression tests, and core workflow guidance.

Next iteration: preserve and document authorized checkpoint continuation boundaries.

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

* Preserve authorized functional checkpoint continuation

Treat checkpoint restore as a host- and storage-authorized path independent of process-local continuation tokens, and issue fresh authority whenever restored execution pauses again. Cover default and per-run storage, deterministic and custom request IDs, token rotation, and checkpoint-plus-response restore.

Files changed: functional workflow and checkpoint interface guidance, functional checkpoint lifecycle tests, the functional HITL sample, and core workflow guidance.

Next iteration: run the final repository-wide Python validation gates.

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

* Validate Python continuation hardening

Run the complete Python workspace checks, aggregate coverage suite, repository hooks, and core package build from the final combined worktree. Keep the validation iteration code-neutral because all gates pass without corrective changes.

Files changed: none; this commit records the final validation gate.

Blockers: none. Next iteration: no remaining AFK tasks.

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

* Handle functional checkpoint continuation failures

Publish retained continuation state only after checkpoint persistence succeeds, and cover reuse after a transient save failure.

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

Copilot-Session: a8f47743-1cdc-4924-8e1b-667d0d790b78

* Address functional continuation review findings

Add owner recovery for lost tokens, harden malformed token validation, preserve consistent failure surfaces, and keep agent pending state aligned with resumable workflow state.

Document process-local single-use continuation semantics and extend regression coverage across direct, streaming, checkpoint, and agent paths.

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

Copilot-Session: a8f47743-1cdc-4924-8e1b-667d0d790b78

* Handle functional continuation cancellation

Release the workflow run guard when cancellation interrupts resumed user code while keeping the single-use continuation token consumed.

Replace sample assertions with explicit runtime checks and add cancellation regression coverage.

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

Copilot-Session: a8f47743-1cdc-4924-8e1b-667d0d790b78

* Simplify functional workflow instance isolation

Remove continuation-token handling and align functional workflows with the graph workflow ownership model: one stateful instance per logical caller or session.

Add create_instance() for independent callers, document the ownership contract, and cover pending-state isolation between instances.

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

Copilot-Session: a8f47743-1cdc-4924-8e1b-667d0d790b78

* Scope functional workflow checkpoint storage

Do not inherit checkpoint storage when creating an independent workflow instance. Allow hosts to provide an explicitly caller-scoped storage adapter and document that shared checkpoint access requires host authorization and tenant isolation.

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

Copilot-Session: a8f47743-1cdc-4924-8e1b-667d0d790b78

* Require building functional workflow instances

Make @workflow return a stateless FunctionalWorkflowDefinition and require build() before run() or as_agent(). This aligns functional workflows with the graph definition/build lifecycle and prevents module-level decorated definitions from retaining caller state.

Move checkpoint configuration to build(), export the definition type, migrate samples, and cover isolated built instances.

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

Copilot-Session: a8f47743-1cdc-4924-8e1b-667d0d790b78

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a8f47743-1cdc-4924-8e1b-667d0d790b78
2026-08-14 00:31:52 +00:00
Evan Mattson 8c4da3c3b9 Python: Harden AG-UI approval lifecycle and resume semantics (#7594)
* Route local approvals through lifecycle owner

Key decisions:
- Add an internal typed approval lifecycle with pending, claimed, executing, and settled states.
- Keep authorization separate from execution; only LocalPendingToolTransitionOwner invokes approved local calls.
- Register server-owned occurrences before canonical ResumeDecision claims and retain one replayable result under the original call identity.

Files changed:
- packages/ag-ui/agent_framework_ag_ui/_approval_lifecycle.py
- packages/ag-ui/agent_framework_ag_ui/_approval_state.py
- packages/ag-ui/agent_framework_ag_ui/_agent_run.py
- packages/ag-ui/tests/ag_ui/test_approval_lifecycle.py
- packages/ag-ui/tests/ag_ui/test_approval_result_event.py
- packages/ag-ui/tests/ag_ui/test_agent_wrapper_comprehensive.py

Verification:
- 952 AG-UI tests passed.
- Focused lifecycle/public tracer passed with warnings treated as errors.
- Ruff format/check and AG-UI Pyright passed.
- git diff --check passed.

Notes for next iteration:
- The function-calling-loop scenario mapping is inaccessible under the organization content-exclusion policy and could not be updated.
- The workspace Poe package fan-out is blocked by the pre-existing missing packages/durabletask/pyproject.toml; equivalent package-local checks were run.

* Make approval batches occurrence-safe

Key decisions:
- Give each local approval a scoped logical occurrence identity and share one occurrence across trusted thread aliases.
- Validate complete Resume Decision batches before applying claims, then account for accepted, rejected, and cancelled occurrences independently.
- Preserve sibling authority and original result identity across failures, mixed decisions, and reused raw call IDs.

Files changed:
- packages/ag-ui/agent_framework_ag_ui/_approval_lifecycle.py
- packages/ag-ui/agent_framework_ag_ui/_approval_state.py
- packages/ag-ui/agent_framework_ag_ui/_agent_run.py
- packages/ag-ui/tests/ag_ui/test_approval_lifecycle.py
- packages/ag-ui/tests/ag_ui/test_agent_wrapper_comprehensive.py

Verification:
- 959 AG-UI tests passed with 90% lifecycle branch coverage.
- 30 focused lifecycle/public tracer tests passed with warnings treated as errors.
- Ruff format/check and AG-UI Pyright passed.
- git diff --check passed.

Notes for next iteration:
- The function-calling-loop scenario mapping remains inaccessible under the organization content-exclusion policy.
- Workspace typing fan-out remains blocked by the pre-existing missing packages/durabletask/pyproject.toml; package-local Pyright passed, while package-local MyPy retains three unrelated baseline errors.

* Make approval resume retries idempotent

Key decisions:
- Retain terminal decisions and outcomes by scoped occurrence so identical accepted and rejected retries reproject results without granting execution authority again.
- Reject conflicting names, arguments, decisions, wrong-scope lookups, and expired authority before an execution intent can reach the local transition owner.
- Keep protocol normalization in the runner while using server-owned lifecycle context to canonicalize retries and preserve existing AG-UI wire aliases.

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

Verification:
- 965 AG-UI tests passed with 90% approval lifecycle coverage.
- 18 focused lifecycle, hostile-resume, wrong-thread, and endpoint retry tests passed with runtime and deprecation warnings treated as errors.
- Ruff format/check and AG-UI package-local Pyright passed.
- git diff --check passed.

Notes for next iteration:
- Terminal retention is process-local and unbounded until the later bounded-retention issue adds its explicit policy.
- The function-calling-loop scenario mapping remains inaccessible under the organization content-exclusion policy.

* Separate approval execution ownership

Key decisions:
- Carry explicit local, hosted, deferred in-run, or unavailable ownership on every approval occurrence and authorized intent.
- Keep lifecycle authorization separate from execution; local calls execute only through the local adapter while hosted and setup-injected decisions forward through owner-specific adapters.
- Leave declaration-only calls pending when no transition owner can act, and settle forwarded outcomes against the original occurrence without local fallback.

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

Verification:
- 967 AG-UI tests passed with 92% package coverage and 89% approval lifecycle coverage.
- 94 focused lifecycle, hosted, deferred-owner, hostile-resume, and approval tests passed.
- Ruff format/check and package-local Pyright passed.
- git diff --check passed.

Notes for next iteration:
- Executing-without-outcome recovery remains for the indeterminate execution-window issue.
- The function-calling-loop scenario mapping remains inaccessible under the organization content-exclusion policy.
- Workspace Poe fan-out remains blocked by the pre-existing missing packages/durabletask/pyproject.toml; equivalent package-local checks passed.

* Represent approval execution uncertainty

Key decisions:
- Distinguish reserved claims from execution windows that may have started an external side effect.
- Recover non-idempotent execution failures as indeterminate and reject identical retries without another invocation.
- Permit claim release only under an explicit safe policy and execution retry only with a predeclared idempotency key shared by local and forwarded owners.

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

Verification:
- 972 AG-UI tests passed with 92% line coverage and 89% package branch coverage.
- 23 focused lifecycle, duplicate-resume, hosted-owner, and public settlement-window tests passed.
- Package-local Ruff and Pyright passed; git diff --check passed.

Notes for next iteration:
- The function-calling-loop scenario mapping remains inaccessible under the organization content-exclusion policy.
- Workspace Poe fan-out remains blocked by the pre-existing missing packages/durabletask/pyproject.toml; equivalent package-local checks passed.

* Reconcile approval snapshots with lifecycle state

Key decisions:
- Keep Approval State authoritative and emit typed snapshot reconciliation keyed by logical occurrence identity.
- Retire settled, rejected, cancelled, expired, indeterminate, and missing controls while preserving nonterminal authority.
- Reconcile stale snapshots before hydration or resume, and retain lifecycle deduplication when snapshot saves fail.

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

Verification:
- 975 AG-UI tests passed with 92% package coverage and 89% approval lifecycle coverage.
- Package-local Ruff and Pyright passed; git diff --check passed.

Notes for next iteration:
- The function-calling-loop scenario mapping remains inaccessible under the organization content-exclusion policy.
- Workspace Poe fan-out remains blocked by the pre-existing missing packages/durabletask/pyproject.toml; equivalent package-local checks passed.

* Bound process-local approval lifecycle state

Key decisions:
- Protect pending, claimed, executing, and indeterminate occurrences from eviction while retaining terminal outcomes for a configurable 15-minute process-local deduplication window.
- Serialize complete approval batches by logical occurrence locks so aliases share atomic decisions and independent batches can progress concurrently.
- Fail capacity, claim, and settlement conflicts explicitly, and emit redacted structured lifecycle telemetry without tool names, arguments, or approval payloads.
- Remove legacy LRU eviction paths so active Approval State and middleware state are never silently discarded.

Files changed:
- packages/ag-ui/agent_framework_ag_ui/_approval_lifecycle.py
- packages/ag-ui/agent_framework_ag_ui/_approval_state.py
- packages/ag-ui/agent_framework_ag_ui/_agent_run.py
- packages/ag-ui/tests/ag_ui/test_approval_lifecycle.py
- packages/ag-ui/tests/ag_ui/test_approval_state.py

Verification:
- 982 AG-UI tests passed with 92% package coverage and 91% approval lifecycle coverage.
- 34 focused lifecycle and storage tests passed with RuntimeWarning and DeprecationWarning treated as errors.
- Package-local Ruff and Pyright passed; git diff --check passed.

Notes for next iteration:
- The function-calling-loop scenario mapping remains inaccessible under the organization content-exclusion policy.
- Workspace Poe fan-out remains blocked by the pre-existing missing packages/durabletask/pyproject.toml; equivalent package-local checks passed.

* Complete approval lifecycle cutover

Key decisions:
- Make ApprovalLifecycle the sole owner of trusted aliases, occurrence metadata, authority transitions, and retained outcomes.
- Remove the parallel mutable pending-approval registry and route local, hosted, deferred, cancellation, replay, and snapshot reconciliation through lifecycle occurrences.
- Encapsulate middleware Approval State behind copy-isolated store methods while keeping AG-UI protocol normalization and event projection in the runner.

Files changed:
- packages/ag-ui/AGENTS.md
- packages/ag-ui/agent_framework_ag_ui/_agent.py
- packages/ag-ui/agent_framework_ag_ui/_agent_run.py
- packages/ag-ui/agent_framework_ag_ui/_approval_lifecycle.py
- packages/ag-ui/agent_framework_ag_ui/_approval_state.py
- packages/ag-ui/tests/ag_ui/test_agent_wrapper_comprehensive.py
- packages/ag-ui/tests/ag_ui/test_approval_lifecycle.py
- packages/ag-ui/tests/ag_ui/test_approval_result_event.py
- packages/ag-ui/tests/ag_ui/test_approval_state.py
- packages/ag-ui/tests/ag_ui/test_endpoint.py
- packages/ag-ui/tests/ag_ui/test_run.py

Verification:
- 964 package-local AG-UI tests passed with 92% coverage and 90% approval lifecycle coverage.
- 85 warning-strict focused approval tests passed.
- Package-local Ruff and Pyright passed; git diff --check passed.

Notes for next iteration:
- The function-calling-loop scenario mapping remains inaccessible under the organization content-exclusion policy.
- Workspace Poe fan-out remains blocked by the pre-existing missing packages/durabletask/pyproject.toml; equivalent package-local checks passed.

* Align AG-UI approval resumes with protocol

* Align workflow approvals with AG-UI resumes

* Address AG-UI approval review findings

* fix AG-UI test typing checks

* fix AG-UI approval retention and cancellation retries
2026-08-14 00:28:14 +00:00
Evan Mattson 5fafa18569 Python: track agent-hooks feature usage (#7558) 2026-08-14 00:06:33 +00:00
Tao Chen ee27065359 Python: Update agentserver to x.1.0b1 (#7621)
* Update agentserver to 2.1.0

* Update agentserver responses and invocations to x.1.0b1

* Pass platform context to state store provider

* Pass user id

* Correct requirements.txt

* Fix unit tests

* Fix unit tests
2026-08-14 00:02:17 +00:00
Atharva Vichare 9645d33cde Python: Fix Cosmos memory provider calling renamed add_cosmos toolkit API (#7635)
* Python: Fix Cosmos memory provider calling renamed add_cosmos toolkit API

The Agent Memory Toolkit renamed AsyncCosmosMemoryClient.add_cosmos to
upsert_memory with an identical signature. The provider declares
azure-cosmos-agent-memory>=0.2.0b3 with no upper bound, so a resolved
install can expose either name. after_run swallows write errors and only
logs a warning, so on a post-rename toolkit the agent turn still looks
successful while long-term memory silently stops receiving turns.

Resolve the write method once per after_run, preferring upsert_memory and
falling back to add_cosmos, so both ends of the declared range keep working.
Same treatment for the emulator test's direct seed call.

Fixes #7633

* Ponytail comment erased

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

* Clarify TODO comment regarding memory method rename

Updated TODO comment to include author and clarify context , to resolve linting error

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-13 21:25:38 +00:00
pratik wayase 9a06fa3f42 Python: fix(python): add release_session API to prevent BackgroundAgentsProvider memory leaks (#7450)
* fix: add release_session API to prevent BackgroundAgentsProvider memory leaks

* fix: address Copilot review comments on release_session

* fix(harness): make background agent session release race-safe and bounded

* fix (harness): address release_session and review feedback
2026-08-13 19:35:36 +00:00
westey 6d25fb1e9c Remove clear and package source mapping from nuget.config to allow user level config inheritance (#7646) 2026-08-13 16:09:41 +00:00
westey e926ad2859 Python: fix streaming transcript duplication with message injection and per-service-call persistence (#7605)
* Fix ordering issue when streaming with content injection and per-service-call persistence

* Update spec

* Address PR comment

* revert uv.lock changes
2026-08-13 15:04:58 +00:00
Giles Odigwe 7cfa905486 Python: scope under-specified approve-for-session permission decisions (#7607)
* Python: scope under-specified approve-for-session permission decisions

PermissionDecisionApproveForSession carries an optional `approval` (tool
prompts) and an optional `domain` (URL prompts), so it can be constructed
with neither. A bare PermissionDecisionApproveForSession() serializes to
{"kind": "approve-for-session"}, which the Copilot CLI cannot interpret: it
dereferences the absent approval and crashes the CLI process with "Cannot
read properties of undefined (reading 'commandIdentifiers')", taking the
whole run down rather than failing a single tool call.

Wrap the resolved permission handler so such decisions are scoped using the
request that triggered them: shell prompts become an approval for that
prompt's command identifiers, MCP prompts an approval for that server and
tool, URL prompts an approval for that URL's domain, and so on.

The decision is only ever narrowed, never widened. When the prompt reports
can_offer_session_approval=False, or the request kind has no session-scoped
approval (such as a hook prompt), the decision is downgraded to a single-use
approval and a warning is logged. Decisions that already specify a scope are
forwarded unchanged, and handler exceptions still propagate so the SDK's
deny-on-error behavior is preserved.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1b45752e-b602-4117-8304-3c8a8b877e3e

* Fix test-suite type-checker errors for permission-decision normalizer

The permission-handler wrapper returned PermissionHandlerType (the sync-or-async
union), so awaiting its result in tests was rejected by the stricter CI type
checkers (pyrefly, ty, zuban). Give the wrapper a dedicated
AsyncPermissionHandlerType return type, and narrow the awaited result with an
isinstance assert before accessing its scope in the async-handler test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1b45752e-b602-4117-8304-3c8a8b877e3e

* Add regression tests for extension permission approval normalization

Cover the two previously-untested branches of _derive_session_approval:
extension-management preserves the request operation, and
extension-permission-access preserves the extension name. Both assert the
serialized approval payload as well.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1b45752e-b602-4117-8304-3c8a8b877e3e

* Scope URL session approvals only for parser-unambiguous URLs

The URL branch derived the persisted domain with Python's urlparse, but the
Copilot CLI parses URLs with WHATWG semantics. The two disagree on crafted
authorities -- e.g. a backslash before the '@' in
'https://example.com<backslash>@evil.com' resolves to example.com under the CLI
but evil.com under urlparse -- so trusting urlparse could persist a session-wide
approval for an unrelated, attacker-chosen domain, widening authorization.

Add _derive_url_session_domain, which returns a domain only when the URL
contains none of the characters WHATWG and urlparse handle differently
(backslash, tab, newline, carriage return); any ambiguity (or a URL with no
host) narrows the decision to a single-use PermissionDecisionApproveOnce.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1b45752e-b602-4117-8304-3c8a8b877e3e

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1b45752e-b602-4117-8304-3c8a8b877e3e
2026-08-13 01:13:19 +00:00
Ruiming Zhao 3221011427 fix(core): warn when advertised MCP archives are rejected (#7622) 2026-08-13 00:44:55 +00:00
Chinmay V 3aadac53c8 Python: fix(redis): honour a max_messages retention limit of zero (#7470)
* Python: fix(redis): honour a max_messages retention limit of zero

RedisHistoryProvider documents None as the sentinel for unlimited storage,
so max_messages=0 must retain nothing. It retained everything: trimming to
-max_messages emits LTRIM key 0 -1, which is Redis's "keep the whole list",
and the count > max_messages guard is true for any non-empty list, so the
trim ran on every save and did nothing.

Negative values were worse than a no-op. max_messages=-5 emitted
LTRIM key 5 -1, deleting the five oldest messages on every save while the
list still grew without bound.

Handle a limit of zero by deleting the key, which is what clear() in this
class already does, and reject negative values in __init__ alongside the
three ValueErrors it already raises for invalid configuration. None and
positive limits are unchanged.

* Python: never write the payload when Redis retention is disabled

Addresses the automated review on #7470. With max_messages=0 the previous
change still RPUSHed every message and deleted the key afterwards, so the
payload reached Redis - and any AOF or replica stream - before being removed,
and was briefly visible to other readers. Short-circuit instead: drop any
existing history and return before serializing, so nothing is written at all.

Also documents the new ValueError in the Raises: section, and asserts in the
test that the pipeline is never used.

* Python: leave stored history alone when Redis retention is disabled

max_messages=0 deleted the session key. _redis_key omits source_id, so
two providers with the default prefix share {key_prefix}:{session_id},
and the after-run pass persists in reverse provider order - a
zero-retention provider listed first would drop a co-located provider's
just-written history on every turn.

Return before serializing instead: no payload reaches Redis, an AOF or a
replica, and stored history is left as it is. Removing stored history is
what clear() is for.

---------

Co-authored-by: Chinmay V <203952148+chinmayv095@users.noreply.github.com>
2026-08-13 00:44:20 +00:00
Ruiming Zhao 35c6b880f7 Python: Preserve Mistral prompt-cache usage details (#7597)
* Fix Mistral cached token usage

Map prompt cache hits from Mistral chat usage into the standard usage details. Add regression coverage for regular and streaming responses.

* Validate Mistral cached token usage

* fix(mistral): satisfy strict cached token typing

Narrow prompt token details before reading cached_tokens so the Mistral package passes strict Pyright without changing runtime validation.\n\nAddresses https://github.com/microsoft/agent-framework/pull/7597#discussion_r3750712320
2026-08-13 00:43:50 +00:00
Vaibhav Patel 3a5d00be54 Python: add checkpointing support to AgentFrameworkWorkflow.run() in agent-framework-ag-ui (#6646)
* Python: add checkpointing support to AgentFrameworkWorkflow.run() in ag-ui

The ag-ui AgentFrameworkWorkflow.run() previously accepted only a
RunAgentInput payload and exposed no way to use the core workflow's
checkpointing/state-persistence, unlike the core agent-framework workflow
implementations. This left ag-ui workflows without resumable execution.

Add optional checkpoint_storage and checkpoint_id keyword arguments to
run(), threaded through run_workflow_stream() into the core Workflow.run().
This delegates to the existing core capability instead of reinventing it and
keeps the public surface consistent with Workflow.run():

- checkpoint_storage enables checkpoint creation at each superstep boundary.
- checkpoint_id resumes a run from a persisted checkpoint; incoming messages
  are forwarded only as request-info responses (never as a new start-executor
  message) to honor the core's message/checkpoint_id mutual exclusivity, and
  responses + checkpoint_id performs a restore-then-send in one call.

Both can also be supplied via the input_data keys __ag_ui_checkpoint_storage
and __ag_ui_checkpoint_id so the FastAPI endpoint (which calls run(input_data)
positionally) can opt in without changing its call site; explicit keyword
arguments take precedence. Checkpoint resume bypasses the AG-UI thread snapshot
hydration early-returns so it always reaches the core restore path.

Backward compatible: run(input_data) keeps working unchanged, and the
non-checkpoint path still calls run_workflow_stream(input_data, workflow) with
its original two-argument convention. Adds focused tests covering checkpoint
creation, resume-from-checkpoint, input-data-keyed params, and the unchanged
default path.

Fixes #6632.

* Import Executor from the public agent_framework API in ag-ui workflow test

* Fix ag-ui checkpoint resume: preserve thread snapshot, coerce resume responses; fix CI lint/typing

A checkpoint-only resume no longer clobbers the stored AG-UI thread snapshot:
the snapshot builder is seeded with the prior stored history so the saved
snapshot keeps the earlier replayable transcript plus the newly produced output.

Resume responses are now coerced against the post-restore pending requests on a
checkpoint restore, so a JSON function_approval_response resumes through AG-UI
after a cold restore instead of failing with a response-type mismatch.

Also update the test-double workflow run() overrides to match the new keyword-only
parent signature and re-sort the workflow test imports so ruff and the typing
checkers pass.

* Coerce ag-ui resume responses without a second checkpoint restore

Reading pending request_info events for resume-response coercion previously
restored the checkpoint into the live workflow, which invoked every executor's
on_checkpoint_restore hook. workflow.run(checkpoint_id=...) then restored again,
running those hooks a second time. Custom restore hooks are not required to be
idempotent, so this could duplicate restoration work or break workflows that
expect exactly one restore per resume.

Load the persisted WorkflowCheckpoint directly from storage (runtime override
or the workflow's build-time context storage) and read its
pending_request_info_events instead. This exposes the same post-restore pending
set for the resume contract and response coercion without mutating workflow
state or running any restore hook, leaving workflow.run(checkpoint_id=...) as
the single restore per resume.

Add a regression test asserting on_checkpoint_restore runs exactly once on a
checkpointed ag-ui resume.

* Python: rework AG-UI workflow checkpointing onto public configuration surfaces

Checkpoint storage is now configured on AgentFrameworkWorkflow (or the
FastAPI endpoint) instead of being smuggled through input_data keys, and
a run resumes by supplying its checkpoint id in the AG-UI forwarded
props. With storage always in hand, resume-response coercion reads the
pending request set straight from the persisted checkpoint via the
public CheckpointStorage.load(), replacing the private runner-context
fallback, and the core run call forwards checkpoint arguments directly,
relying on core validation for conflicting parameters. Requesting a
resume without configured storage now fails with a clear error.

* Assign endpoint checkpoint storage in a single place

The raw-workflow branch assigned checkpoint_storage at construction and
the wiring block assigned it again. Construct the wrapper bare and let
the wiring block own the assignment; the existing-storage guard keeps
allowing a pre-wrapped runner without storage to adopt the endpoint's.

---------

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
2026-08-12 22:07:52 +00:00
westey 56d13bce4e .NET: Add BackgroundAgentsProvider.ReleaseSessionAsync to cancel and release per-session background tasks (#7602)
* Add the abilty for the caller to release and cancel background tasks

* Improve param validation

* Address PR comments

* Address PR comments.

* Address PR comments: cancel tasks before publishing the release

Set IsReleased and publish the ReleaseCompletion only after the in-flight
tasks have actually been cancelled, so a failure to cancel leaves the
session un-released instead of flagging it as released while its tasks are
still running.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-12 14:25:59 +00:00
Luis Rodriguez 27d82b1567 Python: Ignore non-project workspace glob matches (#7509)
* Python: Ignore non-project workspace glob matches

* test: collect workspace script tests

* test: remove standalone script test

---------

Co-authored-by: Luis Rodriguez <25299418+luisangelrod@users.noreply.github.com>
Co-authored-by: Luis Rodriguez <luis.rodriguez@bcpos.com>
2026-08-11 19:14:18 +00:00
Giles Odigwe 5e52c6a718 Python: Fix ClaudeAgent reusing one SDK client across distinct fresh sessions (#7404)
* Python: Fix ClaudeAgent reusing one SDK client across distinct fresh sessions

RawClaudeAgent kept a single mutable ClaudeSDKClient on the agent instance
and reused it across distinct fresh AgentSession objects, because a fresh
session passes session_id=None and the old reuse check treated that as
"keep the current client". Two independent fresh sessions on one shared
agent instance therefore shared a single provider conversation, so the
second session continued the first session's conversation.

Treat a fresh (None) continuation id as always requiring a new client, so
an unbound session never inherits an existing provider conversation.
Legitimate continuity is preserved: once a session runs, its
service_session_id is written back, so later runs pass a real id and resume
correctly. Guard client selection/creation with an asyncio.Lock so
concurrent runs cannot race between the check and the client assignment.

Add regression tests asserting two fresh sessions produce two clients and
that an explicit continuation id still resumes the existing client.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 598a9fe1-28c5-4db1-88fd-e14acd9340af

* Python: Bind Claude SDK client ownership to each run

Replace the single mutable ClaudeSDKClient stored on the agent with a
per-run client. Because a ClaudeSDKClient represents exactly one provider
conversation, sharing one across distinct sessions collapsed them onto the
same conversation and, for concurrent runs, let a fresh session disconnect
a client another run was still streaming from.

_acquire_client now returns a per-run client (owned) that resumes the
framework session's provider conversation when one exists, and _get_stream
releases it in a finally once the run completes. An injected client is
reused verbatim and left to the caller. The streaming loop moves into
_stream_run so the client is a local per-run value rather than shared agent
state, which keeps distinct sessions isolated even under concurrency.

Continuity is preserved: a session's service_session_id is written back
after each run and forwarded as the resume id on subsequent runs. Replace
the client-lifecycle tests with per-run ownership and end-to-end isolation
tests (two fresh sessions get two separate clients, each disconnected).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 598a9fe1-28c5-4db1-88fd-e14acd9340af

* Python: Close remaining Claude session-isolation gaps

Address three shared-state gaps in the Claude adapter surfaced in review:

- Run-scope structured output: carry the run's structured_output through a
  per-run state holder and a per-run finalizer instead of storing it on the
  agent, so a concurrent run cannot overwrite another run's value before its
  finalizer reads it.
- Bind an injected client to one session: an injected ClaudeSDKClient is a
  single Claude conversation, so bind it to the first session that uses it and
  raise AgentInvalidRequestException if a different session tries to reuse it.
  A no-session run reuses the bound session so multi-turn continuity still
  works; multi-session callers must omit client= or use one agent per session.
- Serialize the injected-client path with an asyncio.Lock so concurrent runs
  cannot race its connect or interleave queries on the one shared client.
  Owned per-run clients stay lock-free.

Update and extend the tests accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 598a9fe1-28c5-4db1-88fd-e14acd9340af

* Python: Bind injected Claude client on provider conversation identity

Compare an injected client's binding on the session's service_session_id
(the Claude conversation identity) rather than the framework-local
session_id, falling back to session_id only when the incoming session has
no provider id yet. A reconstructed session from
get_session(service_session_id=...) carries a fresh session_id but the same
provider conversation, so it now continues the bound conversation instead of
raising. Sessions targeting a different conversation are still rejected.

Add regression tests for reconstructed-same-conversation continuation and
different-conversation rejection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 598a9fe1-28c5-4db1-88fd-e14acd9340af

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 598a9fe1-28c5-4db1-88fd-e14acd9340af
2026-08-11 19:04:08 +00:00
Giles Odigwe 30996433ac Python: Restore Gemini thought_signature on approval replays (#7546)
Gemini 3.x rejects a request whose functionCall parts lack a
thought_signature. The signature was carried as base64 protected_data on a
text_reasoning content and re-attached by adjacency, which requires the
carrier to immediately precede its call. An approval round trip replays the
call with no carrier at all, so the next turn failed with a 400.

Track signatures in a bounded per-client call_id map populated at parse time
from the resolved call_id, and backfill only when the emitted part has no
signature. Also stop clearing the held signature on contents that emit no
Part, so an approval response or an unsigned thought summary between the
carrier and its call no longer drops it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dd0909cd-c7c3-42cb-aef1-1e9a3e64d917
2026-08-11 19:03:57 +00:00
Tao Chen e85b3c8ba8 Python: Fix FHA session ID translation (#7608)
* Fix FHA session ID traslation

* Fix tests

* Address comments and fix tests

* Fix typing

* Show how to use user created sessions

* Update README
2026-08-11 17:30:56 +00:00
Roger Barreto 4ca093371e .NET: Add Options for Hosted Agent to Allow Backend Storage (#7572)
* Let the container choose who stores a hosted turn, and say so when it is stored twice

Turning storage off downstream was unconditional and silent. It is now a container choice, and a
deployment that ends up storing anyway is reported instead of quietly recording the conversation in
two places nothing reconciles.

FoundryResponsesOptions, passed through AddFoundryResponses, carries two settings.
AllowStoredOutputEnabled defaults to false, which is when hosting turns storage off for every run and
checks the result. Setting it to true leaves the agent's own configuration exactly as the container
built it, and nothing is checked, overridden, or refused. IncludeReasoningEncryptedContent applies
while storage is off, asking for the encrypted form of the reasoning tokens so reasoning survives
between turns, mirroring AsIChatClientWithStoredOutputDisabled.

Two checks replace the 400 that used to refuse a session carrying a conversation id. The readiness
probe runs each registered agent with its chat client swapped for one that calls nothing, so the
request the agent builds on its own is visible without leaving the container, and an agent asking for
its responses to be stored keeps the container out of rotation. Per request, a conversation id on the
session after the run means the agent's own service kept the turn, which fails with 501 and leaves
the session unsaved so later turns do not resume onto it. A misconfigured container is a server
problem, not a bad request, hence 5xx.

Only a confirmed "this asks to be stored" fails either check. An agent that is not a ChatClientAgent,
a request shape carrying no such setting, and a run that could not be completed all pass: this
package cannot tell what those would do.

* Rename the stored-session flag to say what it means

* Say plainly what server-side storage does to a hosted turn

* Read the store gate as an allow, and align the messages

The flag that decides whether the session may be saved reads as an allow at every use, while the
test it comes from keeps saying what is not allowed, so neither side has to be read inside out.

The wording now matches what the readiness probe says: server side storage must be off, because with
it on the agent's own service records a conversation and response nothing tracks while the hosted
agent records its own for the same request. The message the readiness probe raises no longer travels
through a shared constant, since each check says its own thing.

* Address the review comments left open on the merged PR

Five points raised on #7525 were marked resolved without a code change, and the code they pointed at
was still there.

A hosted workflow session is now recognised by its full type name, so a session of the same short
name from another namespace is not mistaken for one. The test double moves into the namespace it
stands in for, otherwise it would no longer exercise the check.

The per-run chat history provider is handed over on AgentRunOptions.AdditionalProperties, which
ChatClientAgent copies onto the chat options with precedence, rather than being written onto the
chat options here.

The test that pins down who supplies the history said the agent's own provider is used, while it
asserts the opposite, so it is renamed after what it checks.

Reading a response back in the hosted integration tests no longer swallows every failure: only
"not stored" and "not readable through this endpoint" are, so an expired token or a server fault
cannot be mistaken for an absent response and pass the test.

Also fills in the readiness message for the case where storing is explicitly allowed.

* Address the review on #7572

Four findings, all real, all in code this branch introduced.

A container that allows its own service to keep the conversation was still being handed the platform
history on every turn. That service replays the earlier turns itself, so the model was getting each
of them twice, which is the very thing this work exists to prevent. The history now goes in only
while nothing else holds it: the first turn of such a conversation still gets it, and the service
takes over from there.

A turn that fails for storing downstream was announcing itself as completed first and only then
failing, leaving the caller with two different answers for the same turn. The completed event is now
held back until the run is wound up and the session can be read, because the id of any conversation
the agent's service kept only lands there at the very end.

The readiness probe replaced the chat client but left the agent's chat history provider running, so
a provider backed by a database was reading and writing on every probe, and adding the probe's empty
turn to a real conversation. It is stood down for that run now.

The probe also treated any cancellation as the health check's own, so a timeout inside an agent could
fail readiness. Only a cancellation of the health check's token is left to propagate.

Fixing the completed event turned up a latent problem: the terminal event types are named the same in
two namespaces this file pulls in, and the short name binds to the ones the response stream never
produces, so vt is ResponseCompletedEvent was quietly always false. The three terminal types are
now named explicitly.

* Let the chat history provider carry the conversation

The handler used to read the hosting service's record of the conversation and prepend it to the
input of every run, then work out who should not get it: a resumed workflow by the name of its
session type, and a container whose own service already holds the conversation. Two exceptions, a
type name matched as a string, and a shape where the same turns could arrive from two directions.

An agent that reads its history through a provider is now given one, seeded with that record, for
the length of the run. The turns arrive the way the agent expects them rather than as fresh input,
so nothing is stored back as if it had just been said, and the provider is dropped when the run
ends. Only the new input is passed to the run now.

Everything else supplies its own history and is left alone: an agent built with a provider keeps
using it, an agent whose service keeps the conversation reads it from there, and an agent that is
not a ChatClientAgent, a hosted workflow for instance, carries the conversation in its own session
state and wants only the new input. The workflow session type name check is gone with it.

The session is saved on every turn again. It was being withheld when the agent's own service had
kept the turn, which is a decision about that service, not about the session; nothing this handler
adds for a turn reaches the session anyway.

* Fail a turn to skip its session, and name the store check after what it detects

The session was being withheld from the store on a condition about the agent's own service rather
than about the turn, and guarded by an emptiness check on a key that is never empty. A turn that is
being failed now says so, and only that skips the save. A turn that ends incomplete, waiting on
OAuth consent or interrupted by a shutdown, is not a failure: the caller comes back for it and needs
the state built up so far, the tool approval ids among it.

The session key is resolved once as a value that always exists, so both the load and the save use it
without asking again whether it is there.

CheckNotAllowedStoreUsage and notAllowedStoreUsageDetected now read as what they are: a check for an
agent storing when it should not, and the flag saying it was seen.

* Read a hosted response through the agent client, and only forgive a 404

Reading a response back tried the project-level client first and then the per-agent one, swallowing
403 as well as 404 to get past the first. The project-level client cannot see a hosted agent's
responses at all, so that attempt only ever produced the 403 the catch then had to forgive, and any
other 403, an authorization failure for instance, was read as "nothing is stored" and passed the
test.

Only the per-agent client is used now, and only a 404 counts as not stored. Verified against the
service: a well-formed id it has no response for answers 404 invalid_request_error "Response '...'
not found", the same id through the project-level client answers 403 session_not_accessible, and a
malformed id answers 400. Everything but the 404 now surfaces.

* Move the store setting next to the code that reads and writes it

The two halves of the stored output concern lived in a shared helper: one that installs the factory
turning storage off, and one that reads back what a request would have asked for. Each had exactly
one caller, so the helper only added a hop. They now sit in the converter that builds the request and
in the probe client that inspects it, and the helper keeps just the error the handler throws.

The test double standing in for a hosted workflow session is also gone. It was declared inside the
Workflows namespace because the handler used to recognise a resumed workflow by the full name of its
session type; that comparison no longer exists, so the double only needs to not be a ChatClientAgent.

* Say that the stored output setting could not be determined, which is the case being logged
2026-08-11 16:22:26 +00:00
Peter Ibekwe 8a0731ad92 .NET: Prevent telemetry serialization failures from failing workflows (#7612)
* Prevent telemetry serialization failures from failing workflows

* Address PR comments
2026-08-11 15:16:40 +00:00
Peter Ibekwe 6fff2c9b1f Fix misleading workflow protocol attribute diagnostics (#7609) 2026-08-11 15:16:21 +00:00
Saurish 0d75365331 .NET: Add Cosmos NoSQL vector memory sample (#7552)
* .NET: Add Cosmos NoSQL vector memory sample

* Address Cosmos memory sample review feedback

* Fix Cosmos NoSQL memory sample build

---------

Co-authored-by: nos-redacted <nosxredacted@gmail.com>
2026-08-11 10:33:06 +00:00
Peter Ibekwe db979b616a Python: Improve Json parsing for declarative workflow (#7550)
* Json parsing improvement

* Fix PR comments

* Address PR comments.
2026-08-11 05:09:28 +00:00
Tao Chen d0a4165f17 [BREAKING] Python: Migrate FHA to responses==2.0.0b1 and add Foundry state store (#7533)
* Migrate FHA to responses==2.0.0b1 and add Foundry state store

* Fix session id error

* Fix tests

* Improve tests

* Fix copilot comments

* Address comments

* Revert sample changes

* Address comments

* Add ContextScopedStoreProvider

* Fix type check

* Fix type check

* Export ContextScopedStoreProvider
2026-08-10 05:51:59 +00:00
dependabot[bot] 4357ff5742 Bump postcss (#7529)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.22 to 8.5.25.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.22...8.5.25)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.25
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 02:47:05 +00:00
dependabot[bot] 221f4b6df1 Bump postcss from 8.5.15 to 8.5.25 in /python/packages/devui/frontend (#7493)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.15 to 8.5.25.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.15...8.5.25)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.25
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 02:46:59 +00:00
dependabot[bot] a9f7b2b788 Bump pyrefly from 1.1.1 to 1.2.0 in /python (#7541)
Bumps [pyrefly](https://github.com/facebook/pyrefly) from 1.1.1 to 1.2.0.
- [Release notes](https://github.com/facebook/pyrefly/releases)
- [Commits](https://github.com/facebook/pyrefly/compare/1.1.1...1.2.0)

---
updated-dependencies:
- dependency-name: pyrefly
  dependency-version: 1.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 02:44:59 +00:00
dependabot[bot] adcc3de654 Bump js-yaml from 4.3.0 to 4.3.1 in /python/packages/devui/frontend (#7554)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.3.0 to 4.3.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.1/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.3.0...4.3.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 02:42:11 +00:00
dependabot[bot] 034f5fa119 Bump zuban from 0.9.0 to 0.9.1 in /python (#7545)
Bumps [zuban](https://github.com/zubanls/zubanls-python) from 0.9.0 to 0.9.1.
- [Release notes](https://github.com/zubanls/zubanls-python/releases)
- [Commits](https://github.com/zubanls/zubanls-python/compare/v0.9.0...v0.9.1)

---
updated-dependencies:
- dependency-name: zuban
  dependency-version: 0.9.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 02:41:55 +00:00
Evan Mattson 48e547506b Python: Make encrypted reasoning opt-in for Foundry chat (#7536)
* Python: Make Foundry encrypted reasoning opt-in

* Python: Opt hosted replay test into encrypted reasoning
2026-08-10 02:36:48 +00:00
Peter Ibekwe 5eb3eb745e Improve string parsing in declarative workflows (#7535) 2026-08-07 18:07:56 +00:00
SergeyMenshykh c987529df3 .NET: [BREAKING] Rename to AgentIsolationKeyProvider (#7567)
* Update store isolation documentation

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

Copilot-Session: 824a2e29-e79c-42aa-b28e-aa6c12ec3292

* Rename store isolation key provider

Rename the shared session isolation abstraction to reflect its use for both session and task stores.

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

Copilot-Session: 824a2e29-e79c-42aa-b28e-aa6c12ec3292

* Rename to AgentIsolationKeyProvider per review feedback

Drops the `Store` qualifier and keeps an `Agent` prefix so the type is not
confused with generic isolation-key abstractions from other libraries, while
leaving room for future non-store isolation (memory, retrieval).

- StoreIsolationKeyProvider -> AgentIsolationKeyProvider
- ClaimsIdentityStoreIsolationKeyProvider(+Options) -> ClaimsIdentityAgentIsolationKeyProvider(+Options)
- GetStoreIsolationKeyAsync -> GetIsolationKeyAsync
- UseClaimsBasedStoreIsolation -> UseClaimsBasedAgentIsolation

XML docs now state that the `Agent` prefix identifies the hosting API domain and
does not mean agent instances are isolated.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 824a2e29-e79c-42aa-b28e-aa6c12ec3292

* Update hosting spec for AgentIsolationKeyProvider rename

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 824a2e29-e79c-42aa-b28e-aa6c12ec3292

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 824a2e29-e79c-42aa-b28e-aa6c12ec3292
2026-08-07 14:59:07 +00:00
westey aaaa56bc60 .NET: Store executable function calls bypassed by declaration-only tool calls (#7388)
* Allow storing executable functions when mixed with non-executable

* Address PR review feedback on executable function bypassing

- Guard enumerator acquisition so pending bypassed calls are restored when
  the inner client throws synchronously, before the first MoveNextAsync.
- Always surface buffered streaming updates, even when stripping empties
  them, so metadata such as ConversationId and ResponseId is not discarded.
- Document that the decorator must sit below ApprovalResponseBindingChatClient,
  which drops approval responses that have no recorded request.

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

* Address PR comments

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-07 13:52:31 +00:00
Roger Barreto 18ceb182b1 .NET: Give a hosted agent a single source of conversation history (#7525)
* Read hosted chat history through a provider instead of the request input

The handler used to fetch the platform conversation history and prepend it to the
input of every turn. For a ChatClientAgent that runs in parallel with its own chat
history provider, so the conversation had two sources at once. It also had a hidden
cost: platform items carry no chat-history source marker, so the agent's provider
stored them again as if this turn had written them, leaving a second copy of the
conversation inside the persisted session that then diverges from the platform.

Make the chat history provider the single source for a ChatClientAgent:

- Add FoundryChatHistoryProvider, which reads the conversation through
  ResponseContext.GetHistoryAsync (it already resolves previous_response_id and the
  conversation the request belongs to) and stores nothing, because the platform
  persists the response items itself. An instance is created per request because it
  holds that request's context, and it is passed as a run-scoped override so the host
  does not have to mutate the agent.
- Register it only when the agent was created without a chat history provider. When
  one was supplied at construction, that provider owns the conversation and the
  platform history is not used at all.
- Stop adding the platform history to the input for a ChatClientAgent, since the
  provider now delivers it.

A workflow hosted as an agent is not a ChatClientAgent and has no provider pipeline,
so it keeps receiving the platform history from the handler exactly as before.

* Add regression tests for the duplicated hosted chat history

Cover the three symptoms the previous handler produced, each verified to fail when
the handler is reverted to fetching the platform history into the turn input:

- the conversation the service already keeps was copied into the persisted agent
  session by the default in-memory history provider;
- a custom history provider was asked to write that same conversation into its own
  database, because platform items carry no chat-history source marker and so look
  like content this turn produced;
- an agent with its own provider received both that provider's history and the
  platform's in a single request.

Also state precisely, in the provider's remarks, why nothing is written back: for a
stored request the response orchestrator hands the finished response to its responses
provider, which persists the input and output items that a later turn then reads back
through GetHistoryAsync; for a non-stored request nothing is persisted and nothing is
readable, so the request is self-contained either way.

* Keep unstored turns in the session so mixed conversations stay whole

A conversation can mix turns the service stores with turns it does not. History is
resolved from previous_response_id or the conversation regardless of the current
request's store flag, so an unstored turn still reads the stored ones back, but the
service records nothing for it and a later turn would never see it again.

Reading the platform history through FoundryChatHistoryProvider alone lost those
turns: from the second turn onwards the handler treats the session as a resume and
stops feeding history in, and the provider kept nothing of its own, so an unstored
turn simply vanished from the conversation. A regression test drives three turns of
one conversation, the first stored and the rest not, and without this change the
model receives only [second question, ok, third question]: the stored opening turn
is gone.

Give the provider both halves instead of choosing one:

- reading returns what the service serves, followed by the turns kept in the session,
  which are by definition later than anything the service recorded;
- writing keeps a turn only when the service was not asked to store it, so a stored
  turn is never duplicated and an unstored one is never lost.

The turns are held in the agent session under the provider's own state key, so they
travel with the session the host already persists.

* Refuse a stored turn once a conversation holds unstored ones

A conversation can move between stored and unstored turns, and the unstored ones live
only in the agent session. Going back to a stored turn after that would have the
service record it on top of turns the service never saw, so anyone reading the
conversation back from the service would find an answer with no question. Refuse it
before the model is called instead of writing that gap.

Cover the whole shape with a walkthrough of nine turns over one conversation and three
provider instances, each with its own session:

- an instance that never took an unstored turn starts from the turn the service last
  saved, and does not see another instance's unstored turns;
- an instance that did keeps reading the saved turns and adds its own on top;
- asking such an instance for a stored turn is refused, twice, while unstored turns
  keep working;
- a turn stored from one instance does not appear for another, because it sits on a
  different branch of the conversation and so is not among the turns leading to what
  that other instance last saved.

* Say plainly that kept turns belong to the session

The turns the service was not asked to store are written into the agent session's state
bag under this provider's own state key, and a new provider is built for every request,
so nothing is held on the provider object itself. The walkthrough named its three
threads after provider instances, which read as if the object carried the memory.

Name them after the sessions they are, and add a test that pins the behaviour down: a
turn kept through one provider object is read back by a different one given the same
session, and is absent for one given another session.

* Show which half of the conversation each provider decides

The session decides what is kept, but the provider still decides two things: which
service-side conversation is read, because it holds the request's response context, and
whether the turn is kept at all, because it holds the request's store flag.

Add two tests that separate those from the session:

- two providers reading one session, each built for a request of a different
  conversation, return the same kept turn behind different served turns;
- two providers writing to one session, one for a stored request and one for an
  unstored one, leave only the unstored turn behind.

* Say why a hosted workflow keeps taking history from the handler

The comment stated that a workflow hosted as an agent has no provider pipeline
without saying what that means. It derives from AIAgent directly, so it never calls
a ChatHistoryProvider and does not read the run options' additional properties: the
provider could not reach it even if it were registered.

* Ask the session store whether a turn is a resume

The handler decided that a turn was resuming an existing conversation by looking for
state on the session. That reading broke once the handler itself started writing to the
session before the check: it records the caller's identity there, so a session created
moments earlier already carried state and the very first turn of a conversation looked
like a resume. Its history was then never fetched, and the agent answered knowing
nothing of a conversation the service was already holding. It only showed up when
hosted, because running locally there is no identity to record.

Let the store answer the question instead. GetSessionAsync now returns null when nothing
is stored rather than quietly handing back a new session, so a non-null result means a
prior turn established this session and nothing else has to be inferred. Callers that
just want a usable session can use the new GetOrCreateSessionAsync, which is written in
terms of GetSessionAsync so a store overriding one gets the other for free.

Both store implementations and their tests follow the plain-lookup contract: a miss
creates nothing, deserializes nothing, and touches no directory.

* Drop the experimental marker from an internal type

FoundryChatHistoryProvider is internal, so the attribute reached no caller: the marker
exists to warn people consuming the public surface. It also does not follow from the base
type, which does not carry one, and most internal types in this package have none either.
Removing it leaves two usings behind, so they go as well.

* Stand down the agent's second-manager guard for the host's own provider

An agent refuses a second history manager once the model reports a conversation id of its
own, which happens as soon as the container lets the model keep the conversation. The
guard is meant for an application that configured a provider by hand and would otherwise
end up with two of them. Here the host is the one supplying the provider, deliberately and
for every turn, so the guard was rejecting the arrangement it is hosting: the first turn
failed while streaming, and every later one failed before reaching the model at all.

Turn the three conflict settings off on the agent the host is serving, and let the
provider decide what reaches the model. A test drives two turns of one conversation
against a model that reports a conversation id and asserts both complete.

* Pass a caller's request not to store on to the chat client

A request asking the hosting service not to store the response was honoured there and nowhere else, so the service behind the agent's own chat client kept recording the conversation and reporting an id for it. A caller opting out of storage still ended up with a stored conversation, and the container went on continuing it.

Only that direction travels. Carrying store=true across would either force storage on a container whose author turned it off on purpose or change nothing, since storing is already the default.

* Hand the conversation to the agent's own provider instead of a host one

The host no longer supplies a chat history provider of its own. It writes the turns the service holds into the provider the agent already created for itself, and only when that is the stock in-memory one, so an agent given a provider keeps sole control of its storage and the model receives the conversation once.

A conversation the caller stops asking the service to store moves into the session state and stays there. The session's conversation id no longer names anything the service records and cannot be cleared, so the session is cloned without it on that single turn. Asking for a stored turn afterwards is refused: the service would record a turn whose predecessors it does not hold.

An agent that does not read history through a provider, a hosted workflow for example, is still given its prior turns as input, now marked as chat history so no provider along the way stores them as new.

* Run the agent's own request factory instead of replacing it

ChatClientAgent chains a request's raw representation factory with the agent's by taking the agent's only when the request's returns null. The factory added for an unstored turn always answers, so anything the container configured on the agent's ChatOptions was silently dropped for that turn.

The agent's factory is now invoked first and its result is what carries the setting. A result that is not a CreateResponseOptions belongs to some other chat client, which has no notion of storing a response, so it is handed back untouched.

* Cover a stored conversation that stops being stored and asks again

The refusal was only tested on a conversation the service never stored. Reaching it from a stored one goes through the turn that rebuilds the session without its conversation id, so the mark saying the conversation left the service has to survive that rebuild to be found on the next turn.

* Leave the conversation to the AgentServer storage provider alone

The AgentServer SDK records a hosted turn through its own storage provider, around the handler, and serves the conversation back through ResponseContext.GetHistoryAsync. Anything the container stores of its own is a second conversation that storage provider never sees and no one reconciles.

The handler now takes that history as the single source and hands it to the agent as input alongside this turn's messages. The agent's own provider is replaced for the run by one holding its messages in a field, so a run that calls tools still has what its earlier calls produced while nothing survives the request. The service behind the agent's chat client is asked not to store on every turn, whatever the caller asked of the hosting service.

A session that still carries a conversation id means that service is recording a second conversation regardless, so the turn is refused with a 400 rather than run against something nobody can reconcile.

* Narrow the history skip to a resumed workflow

Withholding the conversation from every agent that is not a ChatClientAgent assumed they all carry it in their own session. A hand-written one that keeps nothing would answer with no history from its second turn on, so the check is now on the session type a workflow runs with, which is what actually accumulates the turns.

The conversation and previous response id tests went with it: the session key falls back to the partition of a freshly minted response id, which never has a session saved for it, so a loaded session already implies one of the two was sent.

Also asks a Chat Completions client not to store, since the setting carries the same name on both OpenAI request shapes.

* Add a live test that a hosted turn is not stored twice

The AgentServer SDK's storage provider records every hosted turn around the handler, and
that record is the conversation the caller reads. The agent's own run inside the container
talks to its own service, and when that service is asked to keep the turn it writes a
second copy of the same exchange, on a trail of its own that nobody reads and nobody
reconciles. The caller's conversation looks clean, so the second copy goes unnoticed.

The new downstream-store scenario runs an ordinary Foundry ChatClientAgent, like the first
hosted agent sample, wrapped so that after the run it appends DOWNSTREAM_ID=<id> to the
reply, carrying whatever its own run left behind. The tests then go looking for that id on
the service: finding it means a second copy exists.

Verified live against a Foundry project. On main both tests fail, reporting a readable id
such as resp_0940e276..., and here the container reports DOWNSTREAM_ID=none and both pass.

* Let the session carry the conversation in the downstream store test

The run options were setting the conversation on every call, which the session already does.
The single turn test now binds the session to the conversation up front, and the multi turn
test starts from the agent's own default session and reads back what the hosted agent kept
for the caller off ChatClientAgentSession once the first turn returns.

Re-verified live: still fails on main, reporting a readable id such as resp_0c07a5e4..., and
still passes here.
2026-08-07 10:02:23 +00:00
westey ec32e86646 .NET: Aggregate usage across looping agents and chat clients (#7539)
* Ensure usage is merged for all looping components

* Add max tool approval loop fixes

* Fix net472 build break in usage aggregation tests

DateTimeOffset.UnixEpoch is not available on .NET Framework 4.7.2, so the
WithAggregatedUsage copy tests failed to compile for that target framework.
Use an explicit DateTimeOffset instead; the specific instant is irrelevant,
the value only needs to be non-default so the copy assertion is meaningful.

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

* Address PR comments

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-07 10:00:27 +00:00
SergeyMenshykh 94bbfb2ac8 .NET: Harden file skill discovery (#7540)
* .NET: Harden file skill discovery

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

Copilot-Session: 8ff072fa-42d6-44b0-b226-2182cfc7639c

* .NET: Handle inaccessible skill directories

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

Copilot-Session: 8ff072fa-42d6-44b0-b226-2182cfc7639c

---------

Copilot-Session: 8ff072fa-42d6-44b0-b226-2182cfc7639c
2026-08-07 09:53:56 +00:00
Giles Odigwe 4b1afd9052 Python: surface Gemini thought summaries as reasoning content (#7488)
Gemini thought-summary parts (part.thought=True) were dropped in _parse_parts, so reasoning never reached ChatResponse.contents. Emit them as text_reasoning content instead, matching OpenAIResponsesClient. Round-trip is safe: _convert_message_contents never re-emits reasoning text as a Part.

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

Copilot-Session: b8aa906e-1408-40c1-9a45-6deb40dc36f8
2026-08-07 03:08:25 +00:00
Giles Odigwe 45c515b8a7 Python: fix CopilotStudioAgent LineTooLong on large activities (#7417)
* Python: fix CopilotStudioAgent LineTooLong on large activities

Bump microsoft-agents-copilotstudio-client to >=1.2.0,<2 and forward a configurable read_bufsize (default 1 MiB) to the underlying aiohttp ClientSession via ConnectionSettings.client_session_settings. Copilot Studio streams each activity as a single SSE data line, so activities larger than aiohttp's 512 KB per-line limit previously raised aiohttp.http_exceptions.LineTooLong. Adds a client_session_settings parameter to CopilotStudioAgent and unit tests covering the default, override, and partial-settings cases.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2766dc09-ab5f-4adc-8627-98361d7ccef0

* Python: apply read_bufsize default to supplied CopilotStudio settings

Address review feedback on the LineTooLong fix: when a user supplies their own ConnectionSettings but no client, inject the read_bufsize default so activities larger than aiohttp's 512 KB per-line limit still stream. Document configuring read_bufsize on the explicit pre-built-client path in the package and sample READMEs and the explicit-settings sample. Add unit tests covering the supplied-settings path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2766dc09-ab5f-4adc-8627-98361d7ccef0

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2766dc09-ab5f-4adc-8627-98361d7ccef0
2026-08-07 03:08:06 +00:00
Giles Odigwe b2a2fcbd87 Python: Add response/request customization hooks to OpenAIChatCompletionClient (#7028)
* Python: Fix reasoning content parsing in OpenAIChatCompletionClient

Fix two issues with reasoning content handling in the Chat Completions
client:

1. (#6979) reasoning_details plaintext buried as encrypted data:
   The client dumped the entire reasoning_details array into
   Content.protected_data without setting Content.text, causing AG-UI
   to emit ReasoningEncryptedValueEvent instead of visible
   ReasoningMessageContentEvent for plaintext reasoning providers
   (e.g. OpenRouter). Now extracts readable text from reasoning_details
   entries into Content.text while preserving protected_data for
   round-trip fidelity.

2. (#6978) Mistral list content causes crash:
   Mistral reasoning models return content as a list of typed chunks
   ([{"type": "thinking", ...}, {"type": "text", ...}]) instead of a
   plain string. _parse_text_from_openai assumed content was always a
   string, causing a Pydantic ValidationError downstream. Now detects
   list content and parses thinking chunks as Content.from_text_reasoning
   and text chunks as Content.from_text.

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

* Fix pyright strict-mode type errors and handle content-as-string shape

- Use cast() for proper type narrowing in _extract_reasoning_text and
  _parse_chunked_content to satisfy pyright strict mode
- Handle {"content": "..."} string shape in _extract_reasoning_text
  (addresses review comment about missing format coverage)

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

* Fix mypy errors: cast list content to Any in tests

model_construct bypasses Pydantic runtime validation but mypy still
checks declared types. Use cast(Any, ...) for the list content args.

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

* Address review comments: summary field, reasoning field, and round-trip

- Add 'summary' field extraction in _extract_reasoning_text for
  reasoning.summary entries from OpenRouter
- Handle message.reasoning and message.reasoning_content top-level
  fields (plaintext reasoning without reasoning_details) in both
  streaming and non-streaming paths
- reasoning_details takes priority when both fields are present
- Preserve original Mistral chunk list in additional_properties
  ('_source_content_list') so _prepare_message_for_openai can
  reconstruct the structured list content for multi-turn reasoning
- Add 5 new tests covering all new behaviors

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

* Fix ruff used-dummy-variable: rename _skip_structured_siblings

Remove leading underscore from _skip_structured_siblings variable since
it is accessed (not a dummy variable). Ruff's used-dummy-variable rule
flags variables with leading underscores that are read.

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

* Fix missing newline at end of test file

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

* Address review: type-agnostic chunk round-trip and reasoning field echo-back

- Honor the _source_content_list marker regardless of the first emitted
  content's type by handling it before the type match, so a chunk list
  beginning with a text chunk still round-trips as one structured message
  (addresses github-actions review comment on results[0]).
- Tag every chunked-content item with a shared _structured_content_group
  id and skip only exact group siblings during serialization, instead of
  suppressing all later text/reasoning content.
- Record provenance of top-level reasoning/reasoning_content fields in
  _reasoning_source_field and echo the value back under the same key on
  the next request, which providers such as vLLM require (addresses
  Kimahriman review comment). Replaces the prior behavior that replayed
  surfaced reasoning as visible answer text.
- Factor the duplicated reasoning parsing into _parse_reasoning_content.
- Add tests for provenance capture, reasoning/reasoning_content round-trip,
  reasoning-only messages, text-first chunk round-trip, and unrelated
  sibling preservation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2f3c0308-51bf-4b66-8b53-87a8546743f5

* Replace provider-specific reasoning logic with configurable parse/prepare hooks

Following review feedback (#7028), keep OpenAIChatCompletionClient free of
provider-specific quirks for 'almost OpenAI-compatible' endpoints. Instead of
branching in core for OpenRouter/vLLM/Mistral, expose two optional callables so
callers adapt the client themselves:

- response_parser (OpenAIChatResponseContentsParser): post-processes the Content
  list parsed from each response choice/streaming delta, to surface non-standard
  fields (e.g. reasoning/reasoning_content/reasoning_details) for display.
- message_preparer (OpenAIChatMessagePreparer): post-processes the outgoing request
  message dicts built from each framework Message, to echo provider-specific fields
  back on later turns (e.g. vLLM reasoning) for multi-turn continuity.

Both default to None (no-op; byte-identical stock OpenAI behavior). This reverts the
provider-specific reasoning/chunked-content parsing and round-trip markers previously
added to core; Mistral chunked content is now handled by agent-framework-mistral.

- Add the two callables to RawOpenAIChatCompletionClient / OpenAIChatCompletionClient
  constructors and invoke them at the parse and prepare seams.
- Export the type aliases from the package and the core lazy openai namespace (+ .pyi).
- Replace the removed-behavior tests with tests for the two hooks.
- Document the hooks in packages/openai/AGENTS.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2f3c0308-51bf-4b66-8b53-87a8546743f5

* Skip non-string content in default text parsing

Structured list content (e.g. Mistral reasoning models returning content as a
list of chunks) was wrapped verbatim into a text Content, producing a malformed
Content whose text is a list that crashes downstream (issue #6978). Default text
parsing now skips non-string content so a configured response_parser receives a
clean slate to expand it. Applies to both streaming and non-streaming paths.

Add tests for the skip and for a response_parser expanding chunked content.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2f3c0308-51bf-4b66-8b53-87a8546743f5

* Address review: hook signature, per-role preparer, robust round-trip

- response_parser now receives the already-selected ChatCompletionMessage /
  ChoiceDelta instead of Choice | ChunkChoice, so callers no longer duplicate the
  streaming dispatch (removes the Any/hasattr pattern from tests). The client owns
  the dispatch; parsers read provider fields directly.
- message_preparer now runs once per Message for every role: the build logic moved
  to _build_openai_messages and the hook is applied at a single exit point in
  _prepare_message_for_openai, so system/developer messages no longer bypass it.
- Round-trip example/test now correlates surfaced reasoning via an
  additional_properties marker on message.contents with bounded, order-aware,
  one-to-one dict removal, instead of fragile request-string matching. Adds a test
  proving an answer whose text equals the reasoning text is no longer dropped.
- Update packages/openai/AGENTS.md for the new parser signature and guidance.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2f3c0308-51bf-4b66-8b53-87a8546743f5

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2f3c0308-51bf-4b66-8b53-87a8546743f5
2026-08-07 02:51:06 +00:00
MohammadHaroonAbuomar 7302d0bf23 Python: agent-hooks interception contract as a first-class experimental core feature (#7515)
* feat(python): add agent-hooks middleware as experimental core feature

Implement the AGENT-HOOKS-0.1 interception contract as a first-class
experimental feature in agent_framework core.

- Single public factory agent_hooks_middleware() returning a private
  agent/chat/function middleware trio (one object per middleware
  category); partial or stacked installs fail closed with loud errors.
- All eight interception points: input/output at the agent seam,
  pre/post_model_call at the chat seam, pre/post_tool_call at the
  function seam, agent_startup/agent_shutdown bracketing each run.
- Fail-closed enforcement throughout: transforms write back into the
  native contexts (messages, arguments, results) or raise; content is
  preserved as Content objects; MiddlewareTermination short-circuits
  are guarded at every seam; enforcement-layer failures halt the run;
  interceptor crashes surface as host_error denies.
- Streaming is fully buffered per spec buffered_output semantics: no
  update egresses before the post_model_call/output verdicts; a deny
  at pull time releases zero updates; run state stays active across
  lazy pulls with cleanup on every exit path.
- Session scoping: per-run by default (startup/shutdown bracket each
  run) or host-owned via emitter/builder parameters for one session
  spanning multiple runs.
- agent-hooks-sdk is an opt-in agent-hooks extra (not in all),
  lazy-imported per the _mcp.py pattern; core imports cleanly without
  it and the factory raises a clear ModuleNotFoundError.
- ExperimentalFeature.AGENT_HOOKS + @experimental decorator, lazy root
  export, typing surface, PACKAGE_STATUS.md entry.
- 55 tests built on real Agent/mock-client flows covering deny-before-
  execution, transform write-back, rich-content preservation, complete
  streaming ordering, error cleanup, concurrency isolation, nested
  agents, and importability without the optional SDK.

Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>

* style(python): unquote ResponseStream annotation per pyupgrade

The pre-commit pyupgrade hook rewrites the quoted forward reference;
ResponseStream is imported at runtime in this module, so the quotes
were unnecessary.

Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>

* refactor(python): address agent-hooks review feedback

Reworks the agent-hooks feature per PR review:

- Verdicts now precede durability: a run-scoped persistence gate
  (_sessions.py) defers per-service-call history persistence and
  after-run provider work until the covering post_model_call/output
  verdict permits; denied content never persists, transforms persist
  post-write-back. Unhooked runs are unchanged (verified against an
  instrumented baseline).
- ResponseStream.buffered_and_gated: a buffered-gate combinator that
  applies the run's pending stream hooks before the gate, then seals
  the stream, so no middleware can rewrite egress after the output
  verdict. Replaces the hand-rolled replay iterator.
- MiddlewareBundle (public, _middleware.py): the factory returns an
  indivisible bundle categorize_middleware splits, making partial
  installs impossible by construction; members are validated at
  construction. Bare (non-sequence) middleware at agent construction
  is now normalized instead of silently dropped, and unrecognized
  middleware logs a warning instead of vanishing.
- Factory split and rename: create_agent_hooks_middleware (per-run
  sessions) and create_agent_hooks_middleware_from_emitter
  (host-owned); the sentinel parameter-diffing is gone.
- Wire conversions live in per-point codec classes owning to_wire and
  write_back. Fixes in that code: tool-call name transforms apply or
  raise; non-object args transforms raise; argument write-back merges
  only changed keys (original values, including bytes, preserved by
  identity); message-list write-back matches by identity, not index.
- function_approval_request objects on the normal return path pass
  through un-emitted, preserving the human approval pause.
- Hosted (service-executed) tool calls surface in the post_model_call
  content projection; the tool-seam limitation is documented.
- Import probe covers the full SDK surface and re-raises as
  missing-extra only for the agent_hooks module; module logger added;
  _json_safe replaced by make_json_safe (which gained bytes support);
  tools_registered uses normalize_tools; dependency-pyright analyzes
  the module again via the test dependency-group.
- Tests: 75 in the feature suite (persistence gating, stream-hook
  sealing, approval passthrough, codec units, bundle validation,
  bare-bundle installs), full core suite green.

Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>

* refactor(python): second review round for agent-hooks

Addresses the second review round on the agent-hooks feature:

- Nested-run persistence ownership: RawAgent.run stamps a run identity
  over the run's dynamic extent (including streaming pulls and result
  hooks); the persistence gate binds to its owning run via an
  offer/adopt handshake keyed to the agent instance and accepts only
  its owner's persists — nested runs persist inline regardless of how
  they were started (tool calls, middleware, custom run loops). The
  tool-seam suspension remains for custom-loop sub-agents invoked as
  tools; the one residual case (custom loop nested in a custom loop
  off the tool path) is fail-closed and documented. Fixes a latent
  pre-existing re-deferral: flush() now drains with the gate context
  suspended, so a nested hooked run's permitted after-run persistence
  no longer re-defers into an enclosing gate.
- as_tool stream_callback consumes the released (verdicted) stream;
  observers cannot see denied or pre-transform content. Both
  directions are regression-tested.
- categorize_middleware gained supported_categories: a bundle member
  landing in a category a call site cannot install raises; bare
  middleware warns like _add_middleware. Wired at the chat-client
  sites and the provider seam.
- ResponseStream.buffered_and_gated owns the re-derivation rule via a
  rederive callable (gates cannot choose released updates) and is
  marked experimental.
- Wire codecs compare with bool-aware equality (Python == equates
  1 == True, which made bool/number transforms look untouched and get
  dropped) and _ToolResultCodec.write_back owns the untouched-wire
  rule via the before value.
- middleware parameters accept a bare middleware or bundle everywhere
  the runtime does (constructors, run overloads, as_agent, telemetry
  and harness layers, foundry); the bare-source rule has a single
  owner in categorize_middleware; bare middleware assigned to the
  attribute now executes (documented behavior change).
- MiddlewareBundle is experimental and validates members; approval
  passthrough, typing-check fixes (ty ignores mypy-coded ignore
  comments), logging, and documentation updates per review.

Test count: 85 feature tests plus 12 new this round across sessions,
middleware, agents; full core suite green; typing checked under
mypy, pyrefly, ty, zuban, and pyright.

Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>

* docs(python): drop previous-behavior notes from middleware docstrings

Per review: docstrings describe current behavior only. The
bare-middleware behavior change stays recorded in the PR description
and commit history.

Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>

* fix(python): gate ownership survives retrying middleware

A retry or fallback middleware issuing a second call_next() gave the
new attempt a fresh run identity that the persistence gate's
first-bind-wins ownership rejected, so the retried attempt's history
persisted inline before the output verdict — a denied response became
durable again. The gate now accumulates every identity adopted
through its own offer ticket: all attempts' persistence stays behind
the one final verdict (deny drops all of it, allow flushes all of
it). Accumulation over rebind-replace is deliberate: rebinding would
flip an earlier attempt's still-running background work from deferred
to inline, which is the fail-open direction. A foreign agent still
cannot bind: tickets are minted only by the covered pipeline's final
handler and adoption is instance-keyed.

Also consolidates the bare-middleware-source rule into a single
_as_middleware_list owner used by every interpretation site (the
harness merge, BaseAgent.__init__, categorize_middleware, both
client-kwargs merges, get_response, SessionContext.extend_middleware),
including the str/bytes exclusion the stray copies missed. The
constructor now stores a copy of the caller's sequence; assign to the
middleware attribute for post-construction changes.

Retry regression tests cover denied and allowed retried runs in both
stream modes and fail with first-bind-wins restored.

Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>

* fix(python): streaming seam runs pipeline descent inside the gate

The streaming agent seam ran call_next() outside the persistence
gate (only _consume entered it later), so a retry middleware that
drained a successful attempt with get_final_response() and discarded
it persisted that attempt's exchange before any verdict existed; a
later deny dropped only the retry attempt's deferred work. The
descent is now wrapped in the gate exactly like the non-streaming
seam: attempt identities adopted during descent are accepted owners,
so in-pipeline draining defers, deny drops every attempt, and a
middleware that raises after draining strands the pending persists
unexecuted. The bind_owner docstring now states the actual soundness
invariant covering both bind sites: every bind comes from a run
inside the covered pipeline.

New tests cover drained-and-discarded attempts (deny and allow, both
stream modes) and a sub-agent tool inside a drained attempt; the
streaming deny variant fails with the gate wrap reverted.

Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>

* fix(python): flush deferred persistence on streaming no-result termination

With the pipeline descent now running inside the persistence gate, a
middleware that drains a successful attempt and then terminates
without a result left that attempt's deferred persistence stranded:
the streaming no-result termination path raised before any flush, so
history of exchanges that really happened and passed their own
verdicts quietly vanished (streaming only; non-streaming already
flushes before its re-raise). The path now flushes before re-raising
the termination, with a state.halted guard first so an enforcement
failure during the drained attempt still strands pending fail-closed
and surfaces the halt, mirroring the non-streaming ordering exactly.

The regression test covers both seams; the streaming variant fails
without the fix.

Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>

---------

Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>
2026-08-07 00:25:09 +00:00
westey 422160eabe Python: Add windows junction detection for skills (#7507)
* Add windows junction detection for skills

* Address PR comment
2026-08-06 09:08:37 +00:00
westey 5a1d96df67 Python: Separate mem0 storage and search scopes (#7531)
* Separate mem0 storage and search scopes

* Apply suggestions from code review

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

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-06 08:32:36 +00:00
Atharva Vichare 74a144085a .NET: Bound the tool-approval auto-approval loop (#7472) (#7474)
* .NET: Bound the tool-approval auto-approval loop (#7472)

`ToolApprovalAgent` re-invoked the inner agent from two unbounded `while (true)`
loops whenever every surfaced approval request was auto-approved. Each pass is a
fresh `InnerAgent.RunAsync` / `RunStreamingAsync` call, so a per-request cap such
as `FunctionInvokingChatClient.MaximumIterationsPerRequest` restarts every time
and cannot bound the chain. Under `AllToolsAutoApprovalRule` a model that keeps
requesting an auto-approved tool therefore drives billable model calls
indefinitely; the reporter measured 100M+ tokens over three days.

Adds `ToolApprovalAgentOptions.MaxAutoApprovalIterations` (default
`ToolApprovalAgent.DefaultMaxAutoApprovalIterations`, 10) and bounds both loops.
Naming, default and `Throw.IfLessThan` validation follow the existing
`LoopAgent.DefaultMaxIterations` / `LoopAgentOptions.MaxIterations` convention in
this assembly.

On reaching the cap the agent takes one final inner turn without auto-approving
again, so a remaining approval request is surfaced to the caller to decide.
Returning early instead would hand back an empty response, because
`ProcessAndQueueOutboundApprovalRequestsAsync` strips every approval request once
they are all auto-approved -- the case the loop exists to avoid. This mirrors the
Python behaviour, which logs and issues one final request with tools disabled
once its iteration budget is spent (`_tools.py`).

Python is not affected: it caps at `DEFAULT_MAX_ITERATIONS` (40) and persists
`attempt_count` in the budget state across approval resumes, so a resumed run
continues the count rather than restarting it.

Tests: the runaway is reproduced on both the streaming and non-streaming paths
with an inner agent that never stops requesting an auto-approved tool. Inner
invocations equal the cap plus the final turn, and scale with the configured cap,
so the assertions fail if the bound is removed.

No sample changes: with the loop bounded, Agent_Step01, Agent_Step06,
Agent_Step07 and Hosted-AgentSkills are safe as written.

* .NET: Add Arrange/Act/Assert comments to the cap constructor test

Matches the test convention documented in dotnet/AGENTS.md and used by the
surrounding tests in this file.

* Increase default max auto approval iterations to 40

* Apply suggestion from @westey-m

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

* Update comments in ToolApprovalAgent.cs

---------

Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
2026-08-05 19:33:51 +00:00
Evan Mattson 594954700a Python: Fix AG-UI conversation correlation across runs (#7430)
* Add single agent AGUI sample

* Fix AG-UI conversation correlation across runs

* Address PR review and code quality feedback

* Correlate AG-UI chat spans across runs

---------

Co-authored-by: Tao Chen <taochen@microsoft.com>
2026-08-05 17:17:53 +00:00
SergeyMenshykh a4d4eafa5e Add CodeQL suppression comment for DevUI proxy validation (#7505)
The proxy target validation in ValidateProxyTarget already ensures
requests stay on the configured backend. Add an inline suppression
comment following the repo's established pattern.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4363ab44-4d9e-41a0-97d3-4ab0b973f0b2
2026-08-05 11:33:41 +00:00
SergeyMenshykh da056275e6 .NET: [Experimental] Extend A2A task store with isolation key scoping (#7504)
* .NET: Add tenant-scoped task store isolation for A2A hosting

Wrap ITaskStore with IsolationKeyScopedTaskStore when a
SessionIsolationKeyProvider is registered, mirroring the existing
session store isolation pattern. This ensures task operations are
scoped per tenant in multi-user deployments.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: adc30d6c-ce66-40bb-933e-9801c2156cda

* fix formatting issue

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: adc30d6c-ce66-40bb-933e-9801c2156cda
2026-08-05 11:33:11 +00:00
Peter Ibekwe 1da571860a Updating version for dotnet release 1.17.0 (#7514) 2026-08-04 21:04:06 +00:00
Peter Ibekwe d56e81357e Fail declarative workflows when an agent returns an error (#7497) 2026-08-04 17:23:49 +00:00
Evan Mattson 5f3ca8f93c Python: Fix AG-UI approval resume at the protocol boundary (#7480)
* Python: Fix Ollama approval resume message handling

* Python: Reject empty Ollama approval resume payload

* Python: Keep AG-UI approval controls out of provider input

* Python: Do not trust pending AG-UI tool results
2026-08-04 15:12:33 +00:00
Scarab Systems 4d3c7844d6 Python: Bound tool result compaction summaries (#7396)
* Python: bound tool result compaction summaries

Keep ToolResultCompactionStrategy from re-inserting oversized tool result payloads through the synthetic summary message by bounding the generated digest text.

Add regression coverage proving a large tool result is not embedded verbatim, keeps a bounded prefix, and marks truncation.

* Python: keep excluded tool results out of compaction digests

Build ToolResultCompactionStrategy digest content from messages still included in the group so a summary cannot restore payloads that an earlier compaction already excluded.

Use the strategy cap constant in the large-payload regression and add coverage for already-excluded tool results.

Validation: uv run pytest packages/core/tests/core/test_compaction.py -q -k 'tool_result_compaction'; uv run ruff check packages/core/agent_framework/_compaction.py packages/core/tests/core/test_compaction.py; uv run ruff format --check packages/core/agent_framework/_compaction.py packages/core/tests/core/test_compaction.py; uv run poe test -P core; uv run poe build -P core; env HOME=/tmp/sds-home XDG_CACHE_HOME=/tmp/sds-cache uv run poe test -A.

* Python: align compaction digest review cleanup

Align ToolResultCompactionStrategy's included-message filter with the module's existing EXCLUDED_KEY boolean semantics.

Make the large-payload regression size scale from _SUMMARY_MAX_CHARS so it continues to exercise truncation if the digest cap changes.

Validation: uv run pytest packages/core/tests/core/test_compaction.py -q -k 'tool_result_compaction'; uv run ruff check packages/core/agent_framework/_compaction.py packages/core/tests/core/test_compaction.py; uv run ruff format --check packages/core/agent_framework/_compaction.py packages/core/tests/core/test_compaction.py; uv run poe test -P core; uv run poe build -P core.

* Python: collapse tool result digest scan

---------

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-08-04 15:05:02 +00:00
Eduard van Valkenburg 07511b80c9 Python: Prevent orphaned local approval responses (#7462)
* Python: Prevent orphaned local approval responses

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

Copilot-Session: 0efaca91-a0a7-46f4-9b81-022385607fe4

* Python: Clarify approval serialization boundaries

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

Copilot-Session: 0efaca91-a0a7-46f4-9b81-022385607fe4

---------

Copilot-Session: 0efaca91-a0a7-46f4-9b81-022385607fe4
2026-08-04 01:05:08 +00:00
CTW_CTWalk e84b5a07c1 Python: fix LocalEvaluator reporting zero-check items as passed (#7399)
LocalEvaluator.evaluate initialized item_passed to True and only ever
cleared it inside the loop over check results. With no checks configured
the loop never runs, so an item with zero scores was recorded as passed:
result_counts reported one pass, all_passed was True, and
raise_for_status() did not raise.

Initialize item_passed from bool(check_results) so an item with no
evaluated checks fails closed. This matches the .NET contract in this
repository, where AgentEvaluationResults.ItemPassed ends with
'return result.Metrics.Count > 0' and is pinned by
LocalEvaluator_WithZeroChecks_ItemsHaveZeroMetricsAndFailAsync.

Add a focused regression covering the counts, all_passed, the empty
score list, and raise_for_status(). Update the LocalEvaluator class and
evaluate() docstrings, which previously described the pass rule without
the zero-check case.

Fixes #7397
2026-08-04 00:23:08 +00:00
Evan Mattson 84d5a5eec1 Consolidate Dependabot dependency updates (#7445)
* Bump AgentMemory from 1.2.0 to 1.3.0

---
updated-dependencies:
- dependency-name: AgentMemory
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* .NET: consolidate #7280 AgentMemory.AgentFramework 1.3.0

* Bump github/codeql-action/init from 4.37.0 to 4.37.3

Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.37.0 to 4.37.3.
- [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/99df26d4f13ea111d4ec1a7dddef6063f76b97e9...e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

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

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

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 9.0.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.37.0 to 4.37.3

Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.37.0 to 4.37.3.
- [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/99df26d4f13ea111d4ec1a7dddef6063f76b97e9...e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81)

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

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

* Bump actions/cache from 5.0.5 to 6.1.0

Bumps [actions/cache](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
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

* Bump actions/checkout from 6.0.2 to 7.0.1

Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...3d3c42e5aac5ba805825da76410c181273ba90b1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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 8.3.2 to 9.0.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/11f9893b081a58869d3b5fccaea48c9e9e46f990...c771a70e6277c0a99b617c7a806ffedaca235ff9)

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

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

* Bump ty from 0.0.60 to 0.0.64 in /python

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

---
updated-dependencies:
- dependency-name: ty
  dependency-version: 0.0.65
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* Bump prek from 0.4.10 to 0.4.11 in /python

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

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

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

* Bump uv from 0.11.29 to 0.11.32 in /python

Bumps [uv](https://github.com/astral-sh/uv) from 0.11.29 to 0.11.32.
- [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.29...0.11.32)

---
updated-dependencies:
- dependency-name: uv
  dependency-version: 0.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* Bump ruff from 0.15.22 to 0.16.0 in /python

Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.22 to 0.16.0.
- [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.22...0.16.0)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.16.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* update uv-build requirement in /python

---
updated-dependencies:
- dependency-name: uv-build
  dependency-version: 0.12.0
  dependency-type: direct:development
...

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

* Python: align workspace pins for #7436-#7439

* Python: support ty 0.0.64 diagnostics for #7436

* Python: apply Ruff 0.16 formatting for #7439

* Update workflow action version annotations

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 00:01:14 +00:00
Tao Chen 8d379168b2 Python: Improve python sample validation workflow (#7350)
* Add skill to replace hardcoded foundry project endpoint and model

* Include more samples and fix migration samples part 1

* Fix migration samples

* Replace Foundry hosted agent validation skill

* Fix hosted agent file sample

* Fix agent result format

* Reorganize jobs

* Update discovery heuristic for apps

* Split agents into even more jobs

* Add toolbox endpoint

* Add more pre configured resources

* Fix using deployed agent sample

* Add sample status

* Add playbook

* Exclude hidden folder in sample discovery

* Install autogen dependencies

* Grant azure search RBAC role

* Increase timeout for magentic

* Build search resouce id deterministically

* Remove grant in the workflow

* Move azure cli login closer to when the sample actually runs

* Refactor playbook

* Fix using deployed agent sample

* Actually save the playbooks

* Fix action syntax error

* Fix magentic sample

* Address copilot comments

* Fix link inspection

* Address comments

* Correct README

* Fix playbook path

* Remove trailing space
2026-08-03 22:28:53 +00:00
Evan Mattson 18997c2fde Python: Give the AG-UI Thread Snapshot lifecycle a single owner module (#7479)
* Python: Give the AG-UI Thread Snapshot lifecycle a single owner module

Both the agent and workflow runners independently implemented the thread
snapshot lifecycle: hydration replay, the load-once stored read, resume
message seeding, the stored/request/deferred-default state overlay, and
the save whose storage failures must never surface on an already-streamed
run. The two copies had already drifted in small ways (one hydrate helper
re-checked a store the caller had verified; the two cancelled-resume-id
helpers differed on missing-id handling).

Introduce ThreadSnapshotSession in _snapshot_session.py as the one owner
of that lifecycle, opened once per run and inert when no store or scope
is configured so callers stop branching on configuration. Rewire both
runners onto it, consolidate _cancelled_resume_interrupt_ids in
_run_common (defensive variant) and _event_messages_to_snapshot_dicts in
the new module, and delete the superseded per-runner copies. The session
interface is covered by dedicated tests; existing suites pin runner
behavior. Public exports are unchanged.

* Python: Narrow AG-UI event types in snapshot session tests

The hydration test accessed run_id, snapshot, and messages on values
typed as BaseEvent, which fails the tests/samples type checkers. Narrow
each event with isinstance assertions before reading its fields.
2026-08-03 21:15:39 +00:00
Vaibhav Patel 5cc1b8e3c3 Python: Add hosted agent sample for the agent harness (#7010)
* Python: Add hosted agent sample for the agent harness

* Disable file providers and fix call_server usage in hosted harness sample

Addresses PR review: disable the harness file-memory and file-access
providers so the headless sample doesn't expose file tools or write
outside storage/, and correct the app.py docstring to match
call_server.py (which takes no prompt argument).

* Python: update hosted harness sample for current APIs

---------

Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
2026-08-03 20:55:47 +00:00
Evan Mattson 9ce55cae00 Python: Remove dead AG-UI orchestration helpers and flatten subpackage (#7426)
The _orchestration/_helpers module had no production callers; its only
importer was its own test file. It also carried a stale fork of the live
metadata sanitization in _agent_run.py: the dead copy truncated oversized
values, behavior the live copy deliberately replaced with drop-plus-warning
because truncation can produce invalid JSON.

Move _tooling.py and _predictive_state.py to the package root and remove
the now-empty _orchestration subpackage. Public exports are unchanged.
2026-08-03 20:46:05 +00:00
Evan Mattson 06c0fc2b10 Python: forward Azure AI Search query-source identity (#7278)
* Forward Azure AI Search query-source identity

* Address query source credential review feedback
2026-08-03 20:45:40 +00:00
Henry Su a74811edec fix(python): preserve falsey EditTableV2 items (#7380) 2026-08-03 16:54:29 +00:00
Peter Ibekwe 9c8151699a Fix Handoff orchestration sample not responding to user input (#7442) 2026-08-03 16:40:16 +00:00
NekoPunch f5dfb1413e Python: Add Mistral chat client (#7392)
* feat(python): add Mistral chat client

Implements native Mistral support (#7366) with streaming, tool calling,
and structured output. Talks to the REST API directly over httpx: the
mistralai SDK's pinned OpenTelemetry deps conflict with the workspace.

* refactor(python): simplify Mistral client per review

Drop the streamed tool-call accumulator and multi-choice parsing in
favor of the framework's built-in fragment merging, mark n unsupported,
omit unset strict from json_schema, and leave CI secret wiring to
maintainers.

* test(python): drop n forwarding assertion

n is typed as unsupported on MistralChatOptions; the option-mapping test
still passed n, failing pyrefly/ty/zuban/mypy in CI.

* refactor(python): drop n from MistralChatOptions

n is not part of the base ChatOptions, so removing the key rejects it
without an explicit None override.

* feat(python): mark Mistral feature usage

Both clients flip the shared FeatureIndex.MISTRAL bit before each
request, matching the feature-usage telemetry other providers emit.

* fix(python): key streamed tool calls by index

Mistral omits the tool call id on continuation fragments, and the
framework only coalesces empty-id fragments into the immediately
preceding call, so interleaved parallel calls merged into the wrong
call with corrupted arguments. Accumulate fragments per (choice,
index) and emit each call only once complete.

* fix(python): restore Mistral SDK client injection

Dropping the mistralai dependency turned the embedding client's
client= parameter into a breaking change for injected SDK clients.
Add http_client= for httpx.AsyncClient and keep client= working:
httpx goes to the REST path, a duck-typed mistralai.Mistral goes
through the legacy SDK path with a DeprecationWarning until the
next major release.

* chore(python): tidy Mistral sample header
2026-08-03 06:29:09 +00:00
Chris Gillum 43309018be .NET and Python: Extract Durable Task and Azure Functions integrations (#7465)
* Extract Durable Task and Azure Functions integrations

Remove the migrated implementations, samples, tests, documentation, and repository wiring now owned by microsoft/agent-framework-durable-extension. Preserve Python compatibility through the agent_framework.azure shim and agent-framework-core[all], and leave customer-facing redirects to the new repository.

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

Copilot-Session: 6181dcf9-857b-43ea-9fd2-fcd6b175ffdd

* Fix feature registry validation after extraction

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

Copilot-Session: 6181dcf9-857b-43ea-9fd2-fcd6b175ffdd

* Narrow external feature package paths

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

Copilot-Session: 6181dcf9-857b-43ea-9fd2-fcd6b175ffdd

---------

Copilot-Session: 6181dcf9-857b-43ea-9fd2-fcd6b175ffdd
2026-08-03 03:02:07 +00:00
Joshua Nwachinemere 10fe3c4c72 Python: Ignore excluded tool results during compaction (#7391)
* Python: Ignore excluded tool results during compaction

* fix: avoid extra compaction message pass
2026-08-03 01:38:21 +00:00
1010 changed files with 43768 additions and 80143 deletions
+2 -1
View File
@@ -2,6 +2,7 @@ dirs:
- .
excludedFiles:
- ./python/CHANGELOG.md
- "**/SKILL.md"
ignorePatterns:
- pattern: "/github/"
- pattern: "./actions"
@@ -26,7 +27,7 @@ ignorePatterns:
- pattern: "https:\/\/dotnet.microsoft.com"
- pattern: "https://github.com/Rel1cx/eslint-react"
# excludedDirs:
# Folders which include links to localhost, since it's not ignored with regular expressions
# Folders which include links to localhost, since it's not ignored with regular expressions
baseUrl: https://github.com/microsoft/agent-framework/
aliveStatusCodes:
- 200
+127 -4
View File
@@ -1,7 +1,130 @@
# Code ownership assignments
# https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
#
# Policy: a PR needs one approval, and it must come from a code owner of the changed
# files ("Require review from Code Owners" + "Required approvals: 1" on `main`).
# A PR touching several CODEOWNERS patterns will request review from the applicable
# code owners, but an approval from any applicable code owner is sufficient to satisfy
# GitHub's required-code-owner review.
#
# Order matters: the LAST matching pattern wins, so a module rule fully replaces the
# catch-all rather than adding to it. @chetantoshniwal is included on every line as a
# repository-wide fallback owner. All owners on a line have equal approval authority.
#
# CONVENTION: owners are written in the order
# @chetantoshniwal <owner A> <owner B> [...]
# @chetantoshniwal is at the beginning for aesthetics. The owners share equal approval
# power and responsibility.
#
# RULE: every path must list at least two owners besides @chetantoshniwal. An author
# cannot approve their own PR, so a path with a single module owner leaves only Chetan
# to review whenever that owner is the author, which defeats the point of naming a
# module owner.
#
# Samples: owned by all core developers of that language, not by the module a sample
# demonstrates. Any core Python developer can approve any Python sample, and any core
# .NET developer can approve any .NET sample. No dedicated rule is needed -- samples
# fall through to the /python and /dotnet rules, which already list those developers.
#
# Tests: same as samples. Tests that live inside a package (python/packages/<pkg>/tests)
# are covered by that package's rule instead, since they sit under its path.
# Default owners for everything not matched by a module rule below.
* @chetantoshniwal @westey-m
# Repository-level paths: every core Agent Framework developer is a code owner, so any
# one of them can approve. Be explicit now and we can use the AgentFramework team in the future.
/docs/ @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
/*.md @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
/LICENSE @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
/.gitattributes @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
/.gitignore @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
/.github @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
# Repository-level paths that require specific owners
/.devcontainer @chetantoshniwal @westey-m @rogerbarreto @SergeyMenshykh
/declarative-agents @chetantoshniwal @moonbox3 @peibekwe
# Core Python developers: @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU
/python @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU
# Python packages
/python/packages/a2a/ @chetantoshniwal @giles17 @eavanvalkenburg
/python/packages/ag-ui/ @chetantoshniwal @moonbox3 @giles17
/python/packages/anthropic/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/azure-ai-search/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/azure-contentunderstanding/ @chetantoshniwal @giles17 @eavanvalkenburg
/python/packages/azure-cosmos/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/azure-cosmos-memory/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/bedrock/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/chatkit/ @chetantoshniwal @moonbox3 @giles17
/python/packages/claude/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/copilotstudio/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/core/ @chetantoshniwal @eavanvalkenburg @moonbox3 @TaoChenOSU @giles17
/python/packages/core/agent_framework/_workflows/ @chetantoshniwal @moonbox3 @TaoChenOSU
/python/packages/core/agent_framework/_harness/ @chetantoshniwal @westey-m @eavanvalkenburg
/python/packages/declarative/ @chetantoshniwal @moonbox3 @peibekwe
/python/packages/devui/ @chetantoshniwal @eavanvalkenburg @moonbox3
/python/packages/foundry/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU @moonbox3 @giles17
/python/packages/foundry_hosting/ @chetantoshniwal @TaoChenOSU @eavanvalkenburg
/python/packages/foundry_local/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/gemini/ @chetantoshniwal @giles17 @eavanvalkenburg
/python/packages/github_copilot/ @chetantoshniwal @giles17 @eavanvalkenburg
/python/packages/hosting/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU
/python/packages/hosting-a2a/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU
/python/packages/hosting-mcp/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU
/python/packages/hosting-responses/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU
/python/packages/hosting-telegram/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU
/python/packages/hyperlight/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/lab/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU
/python/packages/mem0/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/mistral/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/monty/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/ollama/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/openai/ @chetantoshniwal @eavanvalkenburg @moonbox3 @TaoChenOSU @giles17
/python/packages/orchestrations/ @chetantoshniwal @moonbox3 @TaoChenOSU
/python/packages/purview/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/redis/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/tools/ @chetantoshniwal @eavanvalkenburg @giles17
# Core .NET developers: @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
# .NET projects
/dotnet/src/Aspire.Hosting.AgentFramework.DevUI/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/LegacySupport/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Shared/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.A2A/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Abstractions/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.AGUI/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Anthropic/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.CopilotStudio/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.Declarative/ @chetantoshniwal @peibekwe @westey-m
/dotnet/src/Microsoft.Agents.AI.DevUI/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Foundry/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.Harness/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.Hosting/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hyperlight/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.Mcp/ @chetantoshniwal @westey-m @peibekwe
/dotnet/src/Microsoft.Agents.AI.Mem0/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.OpenAI/ @chetantoshniwal @westey-m @rogerbarreto
/dotnet/src/Microsoft.Agents.AI.Purview/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.Tools.Shell/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.Valkey/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.Workflows/ @chetantoshniwal @peibekwe @rogerbarreto
/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ @chetantoshniwal @peibekwe @rogerbarreto
/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/ @chetantoshniwal @peibekwe @rogerbarreto
/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/ @chetantoshniwal @peibekwe @rogerbarreto
/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/ @chetantoshniwal @peibekwe @rogerbarreto
python/packages/azurefunctions/ @microsoft/agentframework-durabletask-developers
python/packages/durabletask/ @microsoft/agentframework-durabletask-developers
python/samples/getting_started/azure_functions/ @microsoft/agentframework-durabletask-developers
python/samples/getting_started/durabletask/ @microsoft/agentframework-durabletask-developers
@@ -1,48 +0,0 @@
name: Azure Functions Integration Test Setup
description: Prepare local emulators and tools for Azure Functions integration tests
runs:
using: "composite"
steps:
- name: Start Durable Task Scheduler Emulator
shell: bash
run: |
if [ "$(docker ps -aq -f name=dts-emulator)" ]; then
echo "Stopping and removing existing Durable Task Scheduler Emulator"
docker rm -f dts-emulator
fi
echo "Starting Durable Task Scheduler Emulator"
docker run -d --name dts-emulator -p 8080:8080 -p 8082:8082 -e DTS_USE_DYNAMIC_TASK_HUBS=true mcr.microsoft.com/dts/dts-emulator:latest
echo "Waiting for Durable Task Scheduler Emulator to be ready"
timeout 30 bash -c 'until curl --silent http://localhost:8080/healthz; do sleep 1; done'
echo "Durable Task Scheduler Emulator is ready"
- name: Start Azurite (Azure Storage emulator)
shell: bash
run: |
if [ "$(docker ps -aq -f name=azurite)" ]; then
echo "Stopping and removing existing Azurite (Azure Storage emulator)"
docker rm -f azurite
fi
echo "Starting Azurite (Azure Storage emulator)"
docker run -d --name azurite -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite
echo "Waiting for Azurite (Azure Storage emulator) to be ready"
timeout 30 bash -c 'until curl --silent http://localhost:10000/devstoreaccount1; do sleep 1; done'
echo "Azurite (Azure Storage emulator) is ready"
- name: Start Redis
shell: bash
run: |
if [ "$(docker ps -aq -f name=redis)" ]; then
echo "Stopping and removing existing Redis"
docker rm -f redis
fi
echo "Starting Redis"
docker run -d --name redis -p 6379:6379 redis:latest
echo "Waiting for Redis to be ready"
timeout 30 bash -c 'until docker exec redis redis-cli ping | grep -q PONG; do sleep 1; done'
echo "Redis is ready"
- name: Install Azure Functions Core Tools
shell: bash
run: |
echo "Installing Azure Functions Core Tools"
npm install -g azure-functions-core-tools@4 --unsafe-perm true
func --version
+1 -1
View File
@@ -17,7 +17,7 @@ runs:
using: "composite"
steps:
- name: Set up uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version-file: "python/pyproject.toml"
enable-cache: true
@@ -0,0 +1,19 @@
name: Save Sample Playbooks
description: >
Save the cached sample-validation playbooks. Split out from
sample-validation-setup (which only restores) so the save runs even when the
validation step fails. Combining restore+save via actions/cache would skip the
save on a failing job (post-if: success()), so freshly authored playbooks for
samples that failed validation would never persist. Invoke this with
'if: not-cancelled' after the validation step in each job.
runs:
using: "composite"
steps:
- name: Save sample playbooks cache
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
# Must match the restore path/key in sample-validation-setup/action.yml and the
# sample_validation --playbooks-dir default (samples/sample_validation/playbooks).
path: python/samples/sample_validation/playbooks/
key: sample-playbooks-${{ github.job }}-${{ github.run_id }}
@@ -36,15 +36,31 @@ runs:
shell: bash
run: copilot --version && copilot -p "What can you do in one sentence?"
- name: Set up python and install the project
uses: ./.github/actions/python-setup
with:
python-version: ${{ inputs.python-version }}
os: ${{ inputs.os }}
- name: Restore sample playbooks
# Restore-only. The matching save is a separate step in each job that runs with
# `if: ${{ !cancelled() }}` (see .github/actions/sample-validation-save-playbooks).
# A combined actions/cache would skip its post-job save on a failing job
# (post-if: success()), so playbooks authored for samples that failed validation
# would never persist. Keyed per job so each validate-* job keeps its own playbooks.
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
# Must match the sample_validation --playbooks-dir default, which resolves to
# samples/sample_validation/playbooks (see python/scripts/sample_validation/__main__.py).
# If a job overrides --playbooks-dir, update this path to match.
path: python/samples/sample_validation/playbooks/
key: sample-playbooks-${{ github.job }}-${{ github.run_id }}
restore-keys: |
sample-playbooks-${{ github.job }}-
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ inputs.azure-client-id }}
tenant-id: ${{ inputs.azure-tenant-id }}
subscription-id: ${{ inputs.azure-subscription-id }}
- name: Set up python and install the project
uses: ./.github/actions/python-setup
with:
python-version: ${{ inputs.python-version }}
os: ${{ inputs.os }}
@@ -1,17 +0,0 @@
---
applyTo: "dotnet/src/Microsoft.Agents.AI.DurableTask/**,dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/**"
---
# Durable Task area code instructions
The following guidelines apply to pull requests that modify files under
`dotnet/src/Microsoft.Agents.AI.DurableTask/**` or
`dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/**`:
## CHANGELOG.md
- Each pull request that modifies code should add just one bulleted entry to the `CHANGELOG.md` file containing a change title (usually the PR title) and a link to the PR itself.
- New PRs should be added to the top of the `CHANGELOG.md` file under a "## [Unreleased]" heading.
- If the PR is the first since the last release, the existing "## [Unreleased]" heading should be replaced with a "## v[X.Y.Z]" heading and the PRs since the last release should be added to the new "## [Unreleased]" heading.
- The style of new `CHANGELOG.md` entries should match the style of the other entries in the file.
- If the PR introduces a breaking change, the changelog entry should be prefixed with "[BREAKING]".
+3 -3
View File
@@ -32,13 +32,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -64,6 +64,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4
with:
category: "/language:${{matrix.language}}"
+4 -4
View File
@@ -85,7 +85,7 @@ jobs:
echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
- name: Checkout GitHub automation
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha }}
sparse-checkout: |
@@ -165,7 +165,7 @@ jobs:
steps:
# Safe checkout: base repo only, not the untrusted PR head.
- name: Checkout target repo base
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha }}
fetch-depth: 0
@@ -174,7 +174,7 @@ jobs:
# Private DevFlow checkout: the PAT/token grants access to this repo's code.
- name: Checkout DevFlow
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ env.DEVFLOW_REPOSITORY }}
ref: ${{ env.DEVFLOW_REF }}
@@ -189,7 +189,7 @@ jobs:
python-version: "3.13"
- name: Set up uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: "0.11.x"
enable-cache: true
+7 -118
View File
@@ -38,10 +38,9 @@ jobs:
dotnetChanges: ${{ steps.filter.outputs.dotnet }}
cosmosDbChanges: ${{ steps.filter.outputs.cosmosdb }}
foundryHostingChanges: ${{ steps.filter.outputs.foundryHosting }}
functionsChanged: ${{ steps.filter.outputs.functions }}
coreChanged: ${{ steps.filter.outputs.core }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: filter
with:
@@ -66,13 +65,6 @@ jobs:
- 'dotnet/Directory.Packages.props'
- 'dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/it-build-image.ps1'
- '.github/workflows/dotnet-build-and-test.yml'
functions:
- 'dotnet/src/Microsoft.Agents.AI.DurableTask/**'
- 'dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/**'
- 'dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/**'
- 'dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/**'
- '.github/actions/azure-functions-integration-setup/**'
- '.github/workflows/dotnet-build-and-test.yml'
core:
- 'dotnet/src/Microsoft.Agents.AI/**'
- 'dotnet/src/Microsoft.Agents.AI.Abstractions/**'
@@ -111,7 +103,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: |
@@ -185,7 +177,7 @@ jobs:
runs-on: ${{ matrix.os }}
environment: ${{ matrix.environment }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: |
@@ -242,7 +234,6 @@ jobs:
-OutputPath dotnet/filtered-unit.slnx
./dotnet/eng/scripts/New-FilteredSolution.ps1 @commonArgs `
-TestProjectNameIncludeFilter "*IntegrationTests*" `
-TestProjectNameExcludeFilter "*DurableTask.IntegrationTests*","*AzureFunctions.IntegrationTests*" `
-OutputPath dotnet/filtered-integration.slnx
- name: Run Unit Tests
@@ -364,7 +355,7 @@ jobs:
env:
configuration: Release
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: |
@@ -440,113 +431,11 @@ jobs:
AZURE_SEARCH_INDEX_NAME: ${{ secrets.AZURE_SEARCH_INDEX_NAME }}
# IT_HOSTED_AGENT_IMAGE was exported into $GITHUB_ENV by the previous step.
# DurableTask and AzureFunctions integration tests (ubuntu/net10.0 only).
# Split from main dotnet-test job for path-based filtering and parallelism.
dotnet-test-functions:
needs: [paths-filter]
if: >
github.event_name != 'pull_request' &&
(needs.paths-filter.outputs.functionsChanged == 'true' ||
needs.paths-filter.outputs.coreChanged == 'true' ||
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
environment: integration
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
sparse-checkout: |
.
.github
dotnet
python
declarative-agents
- name: Free runner disk space
uses: ./.github/actions/free-runner-disk-space
- name: Setup dotnet
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
global-json-file: ${{ github.workspace }}/dotnet/global.json
- name: Build functions integration test projects
shell: bash
working-directory: dotnet
run: |
dotnet build ./tests/Microsoft.Agents.AI.DurableTask.IntegrationTests -c Release -f net10.0 --warnaserror
dotnet build ./tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests -c Release -f net10.0 --warnaserror
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Set up Durable Task and Azure Functions Integration Test Emulators
uses: ./.github/actions/azure-functions-integration-setup
id: azure-functions-setup
- name: Run Functions Integration Tests
shell: pwsh
working-directory: dotnet
run: |
# Run DurableTask integration tests
dotnet test `
--project ./tests/Microsoft.Agents.AI.DurableTask.IntegrationTests `
-f net10.0 `
-c Release `
--no-build -v Normal `
--report-xunit-trx `
--report-junit `
--results-directory ../IntegrationTestResults/ `
--ignore-exit-code 8 `
--filter-not-trait "Category=IntegrationDisabled" `
--parallel-algorithm aggressive `
--max-threads 2.0x
# Run AzureFunctions integration tests
dotnet test `
--project ./tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests `
-f net10.0 `
-c Release `
--no-build -v Normal `
--report-xunit-trx `
--report-junit `
--results-directory ../IntegrationTestResults/ `
--ignore-exit-code 8 `
--filter-not-trait "Category=IntegrationDisabled" `
--parallel-algorithm aggressive `
--max-threads 2.0x
env:
# OpenAI Models
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_CHAT_MODEL_NAME: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_REASONING_MODEL_NAME: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
# Azure OpenAI Models
AZURE_OPENAI_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
# 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 }}
- name: Upload functions test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: dotnet-test-results-functions-net10.0-ubuntu-latest
path: IntegrationTestResults/**/*.junit
if-no-files-found: ignore
# This final job is required to satisfy the merge queue. It must only run (or succeed) if no tests failed
dotnet-build-and-test-check:
if: always()
runs-on: ubuntu-latest
needs: [dotnet-build, dotnet-test, dotnet-foundry-hosted-it, dotnet-test-functions]
needs: [dotnet-build, dotnet-test, dotnet-foundry-hosted-it]
steps:
- name: Get Date
shell: bash
@@ -593,13 +482,13 @@ jobs:
github.event_name != 'pull_request' &&
(contains(join(needs.*.result, ','), 'success') ||
contains(join(needs.*.result, ','), 'failure'))
needs: [dotnet-test, dotnet-test-functions]
needs: [dotnet-test]
runs-on: ubuntu-latest
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: |
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
@@ -43,7 +43,7 @@ jobs:
environment: integration
timeout-minutes: 60
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -83,10 +83,6 @@ jobs:
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Set up Durable Task and Azure Functions Integration Test Emulators
if: matrix.os == 'ubuntu-latest'
uses: ./.github/actions/azure-functions-integration-setup
- name: Run Integration Tests
shell: bash
run: |
+1 -1
View File
@@ -41,7 +41,7 @@ jobs:
environment: 'integration'
timeout-minutes: 90
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: |
@@ -25,7 +25,7 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
@@ -42,7 +42,7 @@ jobs:
python-changes: ${{ steps.detect-changes.outputs.python }}
steps:
- name: Check out trusted workflow helpers
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
persist-credentials: false
+4 -4
View File
@@ -68,7 +68,7 @@ jobs:
echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
- name: Checkout scripts
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: |
.github/actions/github-app-token
@@ -135,7 +135,7 @@ jobs:
steps:
# Safe checkout: base repo only.
- name: Checkout target repo base
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
@@ -143,7 +143,7 @@ jobs:
# Private DevFlow (maf-dashboard) checkout.
- name: Checkout DevFlow
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ env.DEVFLOW_REPOSITORY }}
ref: ${{ env.DEVFLOW_REF }}
@@ -158,7 +158,7 @@ jobs:
python-version: "3.13"
- name: Set up uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: "0.11.x"
enable-cache: true
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
issues: write
steps:
- name: Checkout GitHub automation
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: |
.github/actions/github-app-token
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
steps:
- name: Checkout scripts
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: |
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout scripts
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: .github/scripts
fetch-depth: 1
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
is_team_member: ${{ steps.check.outputs.is_team_member }}
steps:
- name: Checkout scripts
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: |
@@ -82,7 +82,7 @@ jobs:
if: ${{ needs.team_check.outputs.is_team_member == 'false' }}
steps:
- name: Checkout scripts
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: |
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-22.04
# check out the latest version of the code
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
+5 -5
View File
@@ -31,7 +31,7 @@ jobs:
env:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up python and install the project
@@ -42,7 +42,7 @@ jobs:
os: ${{ runner.os }}
env:
UV_CACHE_DIR: /tmp/.uv-cache
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/prek
key: prek|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
@@ -68,7 +68,7 @@ jobs:
env:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up python and install the project
@@ -97,7 +97,7 @@ jobs:
env:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up python and install the project
@@ -128,7 +128,7 @@ jobs:
env:
UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up python and install the project
@@ -25,7 +25,7 @@ jobs:
# installability starts differing across supported Python versions.
UV_PYTHON: "3.13"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
+2 -2
View File
@@ -24,9 +24,9 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version-file: "python/pyproject.toml"
enable-cache: true
+12 -80
View File
@@ -48,7 +48,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -81,7 +81,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -127,7 +127,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -178,7 +178,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -192,7 +192,7 @@ jobs:
run: curl -fsSL https://ollama.com/install.sh | sh
working-directory: .
- name: Cache Ollama models
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.ollama/models
key: ollama-models-qwen2.5-1.5b-nomic-embed-text-v1
@@ -232,11 +232,12 @@ jobs:
fallback_url: ${{ env.LOCAL_MCP_URL }}
- name: Prefer local MCP URL when available
run: echo "LOCAL_MCP_URL=${{ steps.local-mcp.outputs.effective_url }}" >> "$GITHUB_ENV"
- name: Test with pytest (Anthropic, Hyperlight, Ollama, MCP integration)
- name: Test with pytest (Anthropic, Hyperlight, Mistral, Ollama, MCP integration)
run: >
uv run pytest --import-mode=importlib
packages/anthropic/tests
packages/hyperlight/tests
packages/mistral/tests
packages/ollama/tests
packages/core/tests/core/test_mcp.py
packages/hosting-mcp/tests
@@ -273,73 +274,6 @@ jobs:
done
kill -KILL -- "-$server_pid" 2>/dev/null || kill -KILL "$server_pid" 2>/dev/null || true
# Azure Functions + Durable Task integration tests
python-tests-functions:
name: Python Integration Tests - Functions
permissions:
contents: read
id-token: write
runs-on: ubuntu-latest
environment: integration
timeout-minutes: 60
env:
UV_PYTHON: "3.11"
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FUNCTIONS_WORKER_RUNTIME: "python"
DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
AzureWebJobsStorage: "UseDevelopmentStorage=true"
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Set up Azure Functions Integration Test Emulators
uses: ./.github/actions/azure-functions-integration-setup
id: azure-functions-setup
- name: Test with pytest (Functions + Durable Task integration)
run: >
uv run pytest --import-mode=importlib
packages/azurefunctions/tests/integration_tests
packages/durabletask/tests/integration_tests
-m integration
-n logical --dist worksteal
-x
--timeout=480 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-functions
path: ./python/pytest.xml
if-no-files-found: ignore
# Foundry integration tests
python-tests-foundry:
name: Python Integration Tests - Foundry
@@ -363,7 +297,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -413,7 +347,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -468,7 +402,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -515,7 +449,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -553,7 +487,6 @@ jobs:
python-tests-openai,
python-tests-azure-openai,
python-tests-misc-integration,
python-tests-functions,
python-tests-foundry,
python-tests-foundry-hosting,
python-tests-cosmos,
@@ -564,7 +497,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.checkout-ref }}
persist-credentials: false
@@ -618,7 +551,6 @@ jobs:
python-tests-openai,
python-tests-azure-openai,
python-tests-misc-integration,
python-tests-functions,
python-tests-foundry,
python-tests-foundry-hosting,
python-tests-cosmos,
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
outputs:
pythonChanges: ${{ steps.filter.outputs.python}}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: filter
with:
@@ -63,7 +63,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up python and install the project
id: python-setup
+14 -96
View File
@@ -36,13 +36,12 @@ jobs:
openaiChanged: ${{ steps.filter.outputs.openai }}
azureChanged: ${{ steps.filter.outputs.azure }}
miscChanged: ${{ steps.filter.outputs.misc }}
functionsChanged: ${{ steps.filter.outputs.functions }}
foundryChanged: ${{ steps.filter.outputs.foundry }}
foundryHostingChanged: ${{ steps.filter.outputs.foundry_hosting }}
cosmosChanged: ${{ steps.filter.outputs.cosmos }}
githubCopilotChanged: ${{ steps.filter.outputs.github_copilot }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: filter
with:
@@ -68,6 +67,7 @@ jobs:
misc:
- 'python/packages/anthropic/**'
- 'python/packages/hyperlight/**'
- 'python/packages/mistral/**'
- 'python/packages/ollama/**'
- 'python/packages/core/agent_framework/_mcp.py'
- 'python/packages/core/tests/core/test_mcp.py'
@@ -76,9 +76,6 @@ jobs:
- '.github/actions/setup-local-mcp-server/**'
- '.github/workflows/python-merge-tests.yml'
- '.github/workflows/python-integration-tests.yml'
functions:
- 'python/packages/azurefunctions/**'
- 'python/packages/durabletask/**'
foundry:
- 'python/packages/foundry/**'
- 'python/samples/**/providers/foundry/**'
@@ -110,7 +107,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -157,7 +154,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -218,7 +215,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -288,7 +285,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -299,7 +296,7 @@ jobs:
run: curl -fsSL https://ollama.com/install.sh | sh
working-directory: .
- name: Cache Ollama models
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.ollama/models
key: ollama-models-qwen2.5-1.5b-nomic-embed-text-v1
@@ -339,11 +336,12 @@ jobs:
fallback_url: ${{ env.LOCAL_MCP_URL }}
- name: Prefer local MCP URL when available
run: echo "LOCAL_MCP_URL=${{ steps.local-mcp.outputs.effective_url }}" >> "$GITHUB_ENV"
- name: Test with pytest (Anthropic, Hyperlight, Ollama, MCP integration)
- name: Test with pytest (Anthropic, Hyperlight, Mistral, Ollama, MCP integration)
run: >
uv run pytest --import-mode=importlib
packages/anthropic/tests
packages/hyperlight/tests
packages/mistral/tests
packages/ollama/tests
packages/core/tests/core/test_mcp.py
packages/hosting-mcp/tests
@@ -390,84 +388,6 @@ jobs:
path: ./python/pytest.xml
if-no-files-found: ignore
# Azure Functions + Durable Task integration tests
python-tests-functions:
name: Python Tests - Functions Integration
needs: paths-filter
if: >
github.event_name != 'pull_request' &&
needs.paths-filter.outputs.pythonChanges == 'true' &&
(github.event_name != 'merge_group' ||
needs.paths-filter.outputs.functionsChanged == 'true' ||
needs.paths-filter.outputs.coreChanged == 'true')
runs-on: ubuntu-latest
environment: integration
env:
UV_PYTHON: "3.11"
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FUNCTIONS_WORKER_RUNTIME: "python"
DURABLE_TASK_SCHEDULER_CONNECTION_STRING: "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
AzureWebJobsStorage: "UseDevelopmentStorage=true"
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
with:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Azure CLI Login
if: github.event_name != 'pull_request'
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Set up Azure Functions Integration Test Emulators
uses: ./.github/actions/azure-functions-integration-setup
id: azure-functions-setup
- name: Test with pytest (Functions + Durable Task integration)
run: >
uv run pytest --import-mode=importlib
packages/azurefunctions/tests/integration_tests
packages/durabletask/tests/integration_tests
-m integration
-n logical --dist worksteal
-x
--timeout=480 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
--junitxml=pytest.xml
working-directory: ./python
- name: Surface failing tests
if: always()
uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2
with:
path: ./python/pytest.xml
summary: true
display-options: fEX
fail-on-empty: false
title: Functions integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-functions
path: ./python/pytest.xml
if-no-files-found: ignore
python-tests-foundry:
name: Python Integration Tests - Foundry
needs: paths-filter
@@ -493,7 +413,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -554,7 +474,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -625,7 +545,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -686,7 +606,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
@@ -730,7 +650,6 @@ jobs:
python-tests-openai,
python-tests-azure-openai,
python-tests-misc-integration,
python-tests-functions,
python-tests-foundry,
python-tests-foundry-hosting,
python-tests-cosmos,
@@ -741,7 +660,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up python and install the project
uses: ./.github/actions/python-setup
with:
@@ -792,7 +711,6 @@ jobs:
python-tests-openai,
python-tests-azure-openai,
python-tests-misc-integration,
python-tests-functions,
python-tests-foundry,
python-tests-foundry-hosting,
python-tests-cosmos,
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
+278 -68
View File
@@ -8,8 +8,11 @@ on:
env:
# Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache
GITHUB_TOKEN: ${{ github.token }}
GITHUB_COPILOT_MODEL: auto
permissions:
copilot-requests: write
contents: read
id-token: write
@@ -20,13 +23,13 @@ jobs:
environment: integration
env:
# Required configuration for get-started samples
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -45,6 +48,10 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 01-get-started --save-report --report-name 01-get-started
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
@@ -54,12 +61,13 @@ jobs:
validate-02-agents:
name: Validate 02-agents
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
# Foundry configuration
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
@@ -67,19 +75,19 @@ jobs:
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_MODEL: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME || vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
# OpenAI configuration
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
# GitHub MCP
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
# Observability
ENABLE_INSTRUMENTATION: "true"
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -105,7 +113,11 @@ jobs:
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents --exclude providers --save-report --report-name 02-agents
cd scripts && uv run python -m sample_validation --subdir 02-agents --exclude providers harness tools --save-report --report-name 02-agents
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
@@ -114,20 +126,110 @@ jobs:
name: validation-report-02-agents
path: python/samples/sample_validation/reports/
validate-02-agents-openai:
name: Validate 02-agents/providers/openai
validate-02-agents-harness:
name: Validate 02-agents/harness
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
# Optional: enables the Foundry memory path in harness samples
FOUNDRY_EMBEDDING_MODEL: ${{ vars.FOUNDRY_EMBEDDING_MODEL || '' }}
FOUNDRY_MEMORY_STORE: ${{ vars.FOUNDRY_MEMORY_STORE || '' }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
echo "FOUNDRY_EMBEDDING_MODEL=$FOUNDRY_EMBEDDING_MODEL" >> .env
echo "FOUNDRY_MEMORY_STORE=$FOUNDRY_MEMORY_STORE" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/harness --save-report --report-name 02-agents-harness
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-02-agents-harness
path: python/samples/sample_validation/reports/
validate-02-agents-tools:
name: Validate 02-agents/tools
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Create .env for samples
run: |
echo "FOUNDRY_PROJECT_ENDPOINT=$FOUNDRY_PROJECT_ENDPOINT" >> .env
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/tools --save-report --report-name 02-agents-tools
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-02-agents-tools
path: python/samples/sample_validation/reports/
validate-02-agents-openai:
name: Validate 02-agents/providers/openai
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_MODEL: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -148,6 +250,10 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/openai --save-report --report-name 02-agents-openai
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
@@ -157,6 +263,7 @@ jobs:
validate-02-agents-azure:
name: Validate 02-agents/providers/azure
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
@@ -167,7 +274,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -187,6 +294,10 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure --save-report --report-name 02-agents-azure
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
@@ -196,6 +307,7 @@ jobs:
validate-02-agents-anthropic:
name: Validate 02-agents/providers/anthropic
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
@@ -205,7 +317,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -224,6 +336,10 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/anthropic --save-report --report-name 02-agents-anthropic
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
@@ -233,20 +349,14 @@ jobs:
validate-02-agents-github-copilot:
name: Validate 02-agents/providers/github_copilot
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
permissions:
copilot-requests: write
contents: read
id-token: write
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_COPILOT_MODEL: claude-opus-4.6
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -260,6 +370,10 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/github_copilot --save-report --report-name 02-agents-github-copilot
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
@@ -278,7 +392,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -292,6 +406,10 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/amazon --save-report --report-name 02-agents-amazon
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
@@ -310,7 +428,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -324,6 +442,10 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/ollama --save-report --report-name 02-agents-ollama
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
@@ -333,19 +455,19 @@ jobs:
validate-02-agents-foundry:
name: Validate 02-agents/providers/foundry
if: false # Temporarily disabled - provider folder also contains the local Foundry sample
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME || '' }}
FOUNDRY_AGENT_VERSION: ${{ vars.FOUNDRY_AGENT_VERSION || '' }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -366,6 +488,10 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/foundry --save-report --report-name 02-agents-foundry
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
@@ -387,7 +513,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -408,6 +534,10 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/copilotstudio --save-report --report-name 02-agents-copilotstudio
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
@@ -423,7 +553,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -437,6 +567,10 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/custom --save-report --report-name 02-agents-custom
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
@@ -446,16 +580,17 @@ jobs:
validate-03-workflows:
name: Validate 03-workflows
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -474,6 +609,10 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 03-workflows --save-report --report-name 03-workflows
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
@@ -481,21 +620,68 @@ jobs:
name: validation-report-03-workflows
path: python/samples/sample_validation/reports/
validate-04-hosting:
name: Validate 04-hosting
if: false # Temporarily disabled because of sample complexity
validate-04-hosting-foundry-hosted-agents:
name: Validate 04-hosting (foundry-hosted-agents)
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
# Foundry hosted agent configuration
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_PROJECT_ID: ${{ vars.FOUNDRY_PROJECT_ID }}
AZURE_CONTAINER_REGISTRY_ENDPOINT: ${{ vars.AZURE_CONTAINER_REGISTRY_ENDPOINT }}
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
TOOLBOX_ENDPOINT: ${{ vars.TOOLBOX_ENDPOINT }}
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_HOSTED_AGENT_NAME }}
MEMORY_STORE_NAME: ${{ vars.FOUNDRY_HOSTED_AGENT_MEMORY_STORE }}
AZURE_SEARCH_ENDPOINT: ${{ vars.AZURE_SEARCH_ENDPOINT }}
AZURE_SEARCH_INDEX_NAME: ${{ vars.FOUNDRY_HOSTED_AGENT_SEARCH_INDEX_NAME }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
with:
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
os: ${{ runner.os }}
- name: Run sample validation
# Maximum parallel workers is set to 1 because all samples use the same port
run: |
cd scripts && uv run python -m sample_validation --subdir 04-hosting/foundry-hosted-agents --save-report --report-name 04-hosting-foundry-hosted-agents --max-parallel-workers 1
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-04-hosting-foundry-hosted-agents
path: python/samples/sample_validation/reports/
validate-04-hosting-other:
name: Validate 04-hosting (other)
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
# A2A configuration
A2A_AGENT_HOST: http://localhost:5001/
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -507,13 +693,17 @@ jobs:
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir 04-hosting --save-report --report-name 04-hosting
cd scripts && uv run python -m sample_validation --subdir 04-hosting --exclude foundry-hosted-agents --save-report --report-name 04-hosting-other
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: validation-report-04-hosting
name: validation-report-04-hosting-other
path: python/samples/sample_validation/reports/
validate-05-end-to-end:
@@ -522,8 +712,8 @@ jobs:
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
@@ -538,7 +728,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -552,6 +742,10 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir 05-end-to-end --save-report --report-name 05-end-to-end
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
@@ -564,21 +758,21 @@ jobs:
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# OpenAI configuration
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
OPENAI_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
defaults:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -598,9 +792,16 @@ jobs:
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
- name: Pre-install AutoGen dependencies for migration samples
run: uv pip install "autogen-agentchat" "autogen-ext[openai]"
- name: Run sample validation
run: |
cd scripts && uv run python -m sample_validation --subdir autogen-migration --save-report --report-name autogen-migration
cd scripts && uv run python -m sample_validation --subdir autogen-migration --save-report --report-name autogen-migration --agent-timeout 600
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
@@ -611,23 +812,25 @@ jobs:
validate-semantic-kernel-migration:
name: Validate semantic-kernel-migration
if: false # Temporarily disabled - to free up Copilot quota for other jobs
runs-on: ubuntu-latest
environment: integration
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
# Azure OpenAI configuration for AF
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration for SK
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
# OpenAI key
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
OPENAI_MODEL: ${{ vars.OPENAI_REASONING_MODEL_NAME }}
# OpenAI configuration for SK
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI_CHAT_MODEL_NAME }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
# Copilot Studio
COPILOTSTUDIOAGENT__ENVIRONMENTID: ${{ secrets.COPILOTSTUDIOAGENT__ENVIRONMENTID }}
COPILOTSTUDIOAGENT__SCHEMANAME: ${{ secrets.COPILOTSTUDIOAGENT__SCHEMANAME }}
@@ -637,7 +840,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup environment
uses: ./.github/actions/sample-validation-setup
@@ -665,6 +868,10 @@ jobs:
run: |
cd scripts && uv run python -m sample_validation --subdir semantic-kernel-migration --save-report --report-name semantic-kernel-migration
- name: Save sample playbooks
if: ${{ !cancelled() }}
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
@@ -679,6 +886,8 @@ jobs:
needs:
- validate-01-get-started
- validate-02-agents
- validate-02-agents-harness
- validate-02-agents-tools
- validate-02-agents-openai
- validate-02-agents-azure
- validate-02-agents-anthropic
@@ -689,12 +898,13 @@ jobs:
- validate-02-agents-copilotstudio
- validate-02-agents-custom
- validate-03-workflows
- validate-04-hosting
- validate-04-hosting-foundry-hosted-agents
- validate-04-hosting-other
- validate-05-end-to-end
- validate-autogen-migration
- validate-semantic-kernel-migration
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download all validation reports
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
@@ -20,7 +20,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download coverage report
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
env:
UV_PYTHON: "3.11"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Save the PR number to a file since the workflow_run event
# in the coverage report workflow does not have access to it
- name: Save PR number
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
run:
working-directory: python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up python and install the project
id: python-setup
uses: ./.github/actions/python-setup
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Get GitHub automation token
id: github-auth
+4 -4
View File
@@ -161,19 +161,19 @@ Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Fram
### Python
- [Getting Started](./python/samples/01-get-started): progressive tutorial from hello-world to hosting
- [Getting Started](./python/samples/01-get-started): progressive tutorial from hello-world to workflows
- [Agent Concepts](./python/samples/02-agents): deep-dive samples by topic (tools, middleware, providers, etc.)
- [Workflows](./python/samples/03-workflows): workflow creation and integration with agents
- [Hosting](./python/samples/04-hosting): A2A, Azure Functions, Durable Task hosting
- [Hosting](./python/samples/04-hosting): A2A, self-hosted protocol helpers, and Foundry hosted agents. Durable Task and Azure Functions samples are in the [Durable Agent Framework extension](https://github.com/microsoft/agent-framework-durable-extension/tree/main/python/samples).
- [End-to-End](./python/samples/05-end-to-end): full applications, evaluation, and demos
### .NET
- [Getting Started](./dotnet/samples/01-get-started): progressive tutorial from hello agent to hosting
- [Getting Started](./dotnet/samples/01-get-started): progressive tutorial from hello agent to workflows
- [Agent Concepts](./dotnet/samples/02-agents/Agents): basic agent creation and tool usage
- [Agent Providers](./dotnet/samples/02-agents/AgentProviders): samples showing different agent providers
- [Workflows](./dotnet/samples/03-workflows): advanced multi-agent patterns and workflow orchestration
- [Hosting](./dotnet/samples/04-hosting): A2A, Durable Agents, Durable Workflows
- [Hosting](./dotnet/samples/04-hosting): A2A and Foundry hosted agents. Durable agent and workflow samples are in the [Durable Agent Framework extension](https://github.com/microsoft/agent-framework-durable-extension/tree/main/dotnet/samples).
- [End-to-End](./dotnet/samples/05-end-to-end): full applications and demos
## Community & Feedback
@@ -14,6 +14,9 @@
# - Knowledge Agent: Performs generic web searches.
# - Coder Agent: Able to write and execute code.
# - Weather Agent: Provides weather information.
#
# Example input:
# Find the current temperatures in Seattle and San Francisco, calculate the difference in Celsius and Fahrenheit, and recommend what clothing to pack for each city.
#
kind: Workflow
maxTurns: 500
@@ -264,14 +267,14 @@ trigger:
output:
messages: Local.Plan
input:
arguments:
team: =Local.TeamDescription
messages: |-
=UserMessage(
"Please briefly explain what went wrong on this last run (the root cause of the failure),
and then come up with a new plan that takes steps and/or includes hints to overcome prior challenges and especially avoids repeating the same mistakes.
As before, the new plan should be concise, be expressed in bullet-point form, and consider the following team composition
(do not involve any other outside people since we cannot contact anyone else):
{Local.TeamDescription}")
As before, the new plan should be concise, be expressed in bullet-point form, and only involve the team members already described
(do not involve any other outside people since we cannot contact anyone else).")
- kind: SetTextVariable
id: setVariable_jW7tmM
-48
View File
@@ -1,48 +0,0 @@
# AGENTS.md
Instructions for AI coding agents working on durable agents documentation.
## Scope
This directory contains feature documentation for the durable agents integration. The source code and samples live elsewhere:
- .NET implementation: `dotnet/src/Microsoft.Agents.AI.DurableTask/` and `dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/`
- Python implementation: `python/packages/durabletask/` and `python/packages/azurefunctions/` (package `agent-framework-azurefunctions`)
- .NET samples: `dotnet/samples/04-hosting/DurableAgents/`
- Python samples: `python/samples/04-hosting/durabletask/`
- Official docs (Microsoft Learn): <https://learn.microsoft.com/agent-framework/integrations/azure-functions>
## Document structure
| File | Purpose |
| --- | --- |
| `README.md` | Main technical overview: architecture, hosting models, orchestration patterns, and links to samples. |
| `durable-agents-ttl.md` | Deep-dive on session Time-To-Live (TTL) configuration and behavior. |
Add new sibling documents when a topic is too detailed for the README (e.g., a new feature like reliable streaming or MCP tool exposure). Keep the README focused on orientation and link out to siblings for depth.
## Writing guidelines
- **Audience**: Developers already familiar with the Microsoft Agent Framework who want to understand what durability adds and how to use it.
- **Host-agnostic first**: Durable agents work in console apps, Azure Functions, and any Durable Taskcompatible host. Show host-agnostic patterns (plain orchestration functions, `IServiceCollection` registration) before Azure Functionsspecific patterns. Avoid giving the impression that Azure Functions is the only hosting option.
- **Both languages**: Always include C# and Python examples side by side. Keep them equivalent in functionality.
- **Callout syntax**: Use GitHub-flavored callouts (`> [!NOTE]`, `> [!IMPORTANT]`, `> [!WARNING]`) rather than bold-text callouts (`> **Note:** ...`).
- **Line length**: Do not wrap long lines. Rely on text viewers / renderers for line wrapping.
- **Tables**: Use spaces around pipes in separator rows (`| --- |` not `|---|`).
- **Code snippets**: Keep them minimal and self-contained. Omit boilerplate (using statements, environment variable reads) unless the snippet is specifically about setup.
- **Cross-references**: Link to Microsoft Learn for conceptual background (Durable Entities, Durable Task Scheduler, Azure Functions). Link to sibling docs within this directory for feature deep-dives.
## Linting
Run markdownlint on all documents before committing, with line-length checks disabled:
```bash
markdownlint docs/features/durable-agents/ --disable MD013
```
## When to update these docs
- A new durable agent feature is added (e.g., a new orchestration pattern, hosting model, or configuration option).
- The public API surface changes in a way that affects how developers use durable agents.
- New sample directories are added — update the sample links in README.md.
- The official Microsoft Learn documentation is restructured — update external links.
+7 -237
View File
@@ -1,239 +1,9 @@
# Durable agents
# Durable Agents Have Moved
## Overview
Durable Task and Azure Functions integrations for Microsoft Agent Framework are now maintained in the [Durable Agent Framework extension repository](https://github.com/microsoft/agent-framework-durable-extension).
Durable agents extend the standard Microsoft Agent Framework with **durable state management** powered by the Durable Task framework. An ordinary Agent Framework agent runs in-process: its conversation history lives in memory and is lost when the process ends. A durable agent persists conversation history and execution state in external storage so that sessions survive process restarts, failures, and scale-out events.
| Capability | Ordinary agent | Durable agent |
| --- | --- | --- |
| Conversation history | In-memory only | Durably persisted |
| Failure recovery | State lost on crash | Automatically resumed |
| Multi-instance scale-out | Not supported | Any worker can resume a session |
| Multi-agent orchestrations | Manual coordination | Deterministic, checkpointed workflows |
| Human-in-the-loop | Must keep process alive | Can wait days/weeks with zero compute |
| Hosting | Any process | Console app, Azure Functions, or any Durable Taskcompatible host |
> [!NOTE]
> For a step-by-step tutorial and deployment guidance, see [Azure Functions (Durable)](https://learn.microsoft.com/agent-framework/integrations/azure-functions) on Microsoft Learn.
## How durable agents work
Durable agents are implemented on top of [Durable Entities](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-entities) (also called "virtual actors"). Each **agent session** maps to one entity instance whose state contains the full conversation history. When you send a message to a durable agent, the following happens:
1. The message is dispatched to the entity identified by an `AgentSessionId` (a composite of the agent name and a unique session key).
2. The entity loads its persisted `DurableAgentState`, which includes the complete conversation history.
3. The entity invokes the underlying `AIAgent` with the full conversation history, collects the response, and appends both the request and the response to the state.
4. The updated state is persisted back to durable storage automatically.
Because the entity framework serializes access to each entity instance, concurrent messages to the same session are processed one at a time, eliminating race conditions.
### Agent session identity
Every durable agent session is identified by an `AgentSessionId`, which has two components:
- **Name** the registered name of the agent (case-insensitive).
- **Key** a unique session key (case-sensitive), typically a GUID.
The session ID is mapped to an underlying Durable Task entity ID with a `dafx-` prefix (e.g., `dafx-joker`). This naming convention is consistent across both .NET and Python implementations.
## Architecture
### .NET
The .NET implementation consists of two NuGet packages:
| Package | Purpose |
| --- | --- |
| `Microsoft.Agents.AI.DurableTask` | Core durable agent types: `DurableAIAgent`, `AgentEntity`, `DurableAgentSession`, `AgentSessionId`, `DurableAgentsOptions`, and the state model. |
| `Microsoft.Agents.AI.Hosting.AzureFunctions` | Azure Functions hosting integration: auto-generated HTTP endpoints, MCP tool triggers, entity function triggers, and the `ConfigureDurableAgents` extension method on `FunctionsApplicationBuilder`. |
Key types:
- **`DurableAIAgent`** A subclass of `AIAgent` used *inside orchestrations*. Obtained via `context.GetAgent("agentName")`, it routes `RunAsync` calls through the orchestration's entity APIs so that each call is checkpointed.
- **`DurableAIAgentProxy`** A subclass of `AIAgent` used *outside orchestrations* (e.g., from HTTP triggers or console apps). It signals the entity via `DurableTaskClient` and polls for the response.
- **`AgentEntity`** The `TaskEntity<DurableAgentState>` that hosts the real agent. It loads the registered `AIAgent` by name, wraps it in an `EntityAgentWrapper`, feeds it the full conversation history, and persists the result.
- **`DurableAgentSession`** An `AgentSession` subclass that carries the `AgentSessionId`.
- **`DurableAgentsOptions`** Builder for registering agents and configuring TTL.
### Python
The core Python implementation is in the `agent-framework-durabletask` package (`python/packages/durabletask`). Azure Functions hosting (including `AgentFunctionApp`) is in the separate `agent-framework-azurefunctions` package (`python/packages/azurefunctions`).
Key types:
- **`DurableAIAgent`** A generic proxy (`DurableAIAgent[TaskT]`) implementing `SupportsAgentRun`. Returns a `TaskT` from `run()` — either an `AgentResponse` (client context) or a `DurableAgentTask` (orchestration context, must be `yield`ed).
- **`DurableAIAgentWorker`** Wraps a `TaskHubGrpcWorker` and registers agents as durable entities via `add_agent()`.
- **`DurableAIAgentClient`** Wraps a `TaskHubGrpcClient` for external callers. `get_agent()` returns a `DurableAIAgent[AgentResponse]`.
- **`DurableAIAgentOrchestrationContext`** Wraps an `OrchestrationContext` for use inside orchestrations. `get_agent()` returns a `DurableAIAgent[DurableAgentTask]`.
- **`AgentEntity`** Platform-agnostic agent execution logic that manages state, invokes the agent, handles streaming, and calls response callbacks.
## Hosting models
### Azure Functions
The recommended production hosting model. A single call to `ConfigureDurableAgents` (C#) or `AgentFunctionApp` (Python) automatically:
- Registers agent entities with the Durable Task worker.
- Generates HTTP endpoints at `/api/agents/{agentName}/run` for each registered agent.
- Supports `thread_id` query parameter / JSON field and the `x-ms-thread-id` response header for session continuity.
- Supports fire-and-forget via the `x-ms-wait-for-response: false` header (returns HTTP 202).
- Optionally exposes agents as MCP tools.
**C# example:**
```csharp
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options => options.AddAIAgent(agent))
.Build();
app.Run();
```
**Python example:**
```python
app = AgentFunctionApp(agents=[agent])
```
### Console apps / generic hosts
For self-hosted or non-serverless scenarios, register durable agents via `IServiceCollection.ConfigureDurableAgents` (.NET) or `DurableAIAgentWorker` (Python) with explicit Durable Task worker and client configuration.
**C# example:**
```csharp
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.ConfigureDurableAgents(
options => options.AddAIAgent(agent),
workerBuilder: b => b.UseDurableTaskScheduler(connectionString),
clientBuilder: b => b.UseDurableTaskScheduler(connectionString));
})
.Build();
```
**Python example:**
```python
worker = DurableAIAgentWorker(TaskHubGrpcWorker(host_address="localhost:4001"))
worker.add_agent(agent)
worker.start()
```
## Deterministic multi-agent orchestrations
Durable agents can be composed into deterministic, checkpointed workflows using Durable Task orchestrations. The orchestration framework replays orchestrator code on failure, so completed agent calls are not re-executed.
### Patterns
| Pattern | Description |
| --- | --- |
| **Sequential (chaining)** | Call agents one after another, passing outputs forward. |
| **Parallel (fan-out/fan-in)** | Run multiple agents concurrently and aggregate results. |
| **Conditional** | Branch orchestration logic based on structured agent output. |
| **Human-in-the-loop** | Pause for external events (approvals, feedback) with optional timeouts. |
### Using agents in orchestrations
Inside an orchestration function, obtain a `DurableAIAgent` via the orchestration context. Each agent gets its own session (created with `CreateSessionAsync` / `create_session`), and you can call the same agent multiple times on the same session to maintain conversation context across sequential invocations.
**C#:**
```csharp
static async Task<string> WritingOrchestration(TaskOrchestrationContext context)
{
// Get a durable agent reference — works in any host (console app, Azure Functions, etc.)
DurableAIAgent writer = context.GetAgent("WriterAgent");
// Create a session to maintain conversation context across multiple calls
AgentSession session = await writer.CreateSessionAsync();
// First call: generate an initial draft
AgentResponse<TextResponse> draft = await writer.RunAsync<TextResponse>(
message: "Write a concise inspirational sentence about learning.",
session: session);
// Second call: refine the draft — the agent sees the full conversation history
AgentResponse<TextResponse> refined = await writer.RunAsync<TextResponse>(
message: $"Improve this further while keeping it under 25 words: {draft.Result.Text}",
session: session);
return refined.Result.Text;
}
```
**Python:**
```python
def writing_orchestration(context, _):
agent_ctx = DurableAIAgentOrchestrationContext(context)
# Get a durable agent reference — works in any host (standalone worker, Azure Functions, etc.)
writer = agent_ctx.get_agent("WriterAgent")
# Create a session to maintain conversation context across multiple calls
session = writer.create_session()
# First call: generate an initial draft
draft = yield writer.run(
messages="Write a concise inspirational sentence about learning.",
session=session,
)
# Second call: refine the draft — the agent sees the full conversation history
refined = yield writer.run(
messages=f"Improve this further while keeping it under 25 words: {draft.text}",
session=session,
)
return refined.text
```
> [!IMPORTANT]
> In .NET, `DurableAIAgent.RunAsync<T>` deliberately avoids `ConfigureAwait(false)` because the Durable Task Framework uses a custom synchronization context — all continuations must run on the orchestration thread.
## Streaming and response callbacks
Durable agents do not support true end-to-end streaming because entity operations are request/response. However, **reliable streaming** is supported via response callbacks:
- **`IAgentResponseHandler`** (.NET) or **`AgentResponseCallbackProtocol`** (Python) Implement this interface to receive streaming updates as the underlying agent generates them (e.g., push tokens to a Redis Stream for client consumption).
- The entity still returns the complete `AgentResponse` after the stream is fully consumed.
- Clients can reconnect and resume reading from a cursor-based stream (e.g., Redis Streams) without losing messages.
See the **Reliable Streaming** samples for a complete implementation using Redis Streams.
## Session TTL (Time-To-Live)
Durable agent sessions support automatic cleanup via configurable TTL. See [Session TTL](durable-agents-ttl.md) for details on configuration, behavior, and best practices.
## Observability
When using the [Durable Task Scheduler](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler) as the durable backend, you get built-in observability through its dashboard:
- **Conversation history** View complete chat history for each agent session.
- **Orchestration visualization** See multi-agent execution flows, including parallel branches and conditional logic.
- **Performance metrics** Monitor agent response times, token usage, and orchestration duration.
- **Debugging** Trace tool invocations and external event handling.
## Samples
- **.NET** [Console app samples](../../../dotnet/samples/04-hosting/DurableAgents/ConsoleApps/) and [Azure Functions samples](../../../dotnet/samples/04-hosting/DurableAgents/AzureFunctions/) covering single-agent, chaining, concurrency, conditionals, human-in-the-loop, long-running tools, MCP tool exposure, and reliable streaming.
- **Python** [Durable Task samples](../../../python/samples/04-hosting/durabletask/) covering single-agent, multi-agent, streaming, chaining, concurrency, conditionals, and human-in-the-loop.
## Packages
| Language | Package | Source |
| --- | --- | --- |
| .NET | `Microsoft.Agents.AI.DurableTask` | [`dotnet/src/Microsoft.Agents.AI.DurableTask`](../../../dotnet/src/Microsoft.Agents.AI.DurableTask) |
| .NET | `Microsoft.Agents.AI.Hosting.AzureFunctions` | [`dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions`](../../../dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions) |
| Python | `agent-framework-durabletask` | [`python/packages/durabletask`](../../../python/packages/durabletask) |
| Python | `agent-framework-azurefunctions` | [`python/packages/azurefunctions`](../../../python/packages/azurefunctions) |
## Further reading
- [Azure Functions (Durable) — Microsoft Learn](https://learn.microsoft.com/agent-framework/integrations/azure-functions)
- [Durable Task Scheduler](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler)
- [Durable Entities](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-entities)
- [Session TTL](durable-agents-ttl.md)
- [.NET source](https://github.com/microsoft/agent-framework-durable-extension/tree/main/dotnet/src)
- [.NET samples](https://github.com/microsoft/agent-framework-durable-extension/tree/main/dotnet/samples)
- [Python source](https://github.com/microsoft/agent-framework-durable-extension/tree/main/python/packages)
- [Python samples](https://github.com/microsoft/agent-framework-durable-extension/tree/main/python/samples)
- [Durable agent documentation](https://github.com/microsoft/agent-framework-durable-extension/tree/main/docs/features/durable-agents)
@@ -1,147 +0,0 @@
# Time-To-Live (TTL) for durable agent sessions
## Overview
The durable agents automatically maintain conversation history and state for each session. Without automatic cleanup, this state can accumulate indefinitely, consuming storage resources and increasing costs. The Time-To-Live (TTL) feature provides automatic cleanup of idle agent sessions, ensuring that sessions are automatically deleted after a period of inactivity.
## What is TTL?
Time-To-Live (TTL) is a configurable duration that determines how long an agent session state will be retained after its last interaction. When an agent session is idle (no messages sent to it) for longer than the TTL period, the session state is automatically deleted. Each new interaction with an agent resets the TTL timer, extending the session's lifetime.
## Benefits
- **Automatic cleanup**: No manual intervention required to clean up idle agent sessions
- **Cost optimization**: Reduces storage costs by automatically removing unused session state
- **Resource management**: Prevents unbounded growth of agent session state in storage
- **Configurable**: Set TTL globally or per-agent type to match your application's needs
## Configuration
TTL can be configured at two levels:
1. **Global default TTL**: Applies to all agent sessions unless overridden
2. **Per-agent type TTL**: Overrides the global default for specific agent types
Additionally, you can configure a **minimum deletion delay** that controls how frequently deletion operations are scheduled. The default value is 5 minutes, and the maximum allowed value is also 5 minutes.
> [!NOTE]
> Reducing the minimum deletion delay below 5 minutes can be useful for testing or for ensuring rapid cleanup of short-lived agent sessions. However, this can also increase the load on the system and should be used with caution.
### Default values
- **Default TTL**: 14 days
- **Minimum TTL deletion delay**: 5 minutes (maximum allowed value, subject to change in future releases)
### Configuration examples
#### .NET
```csharp
// Configure global default TTL and minimum signal delay
services.ConfigureDurableAgents(
options =>
{
// Set global default TTL to 7 days
options.DefaultTimeToLive = TimeSpan.FromDays(7);
// Add agents (will use global default TTL)
options.AddAIAgent(myAgent);
});
// Configure per-agent TTL
services.ConfigureDurableAgents(
options =>
{
options.DefaultTimeToLive = TimeSpan.FromDays(14); // Global default
// Agent with custom TTL of 1 day
options.AddAIAgent(shortLivedAgent, timeToLive: TimeSpan.FromDays(1));
// Agent with custom TTL of 90 days
options.AddAIAgent(longLivedAgent, timeToLive: TimeSpan.FromDays(90));
// Agent using global default (14 days)
options.AddAIAgent(defaultAgent);
});
// Disable TTL for specific agents by setting TTL to null
services.ConfigureDurableAgents(
options =>
{
options.DefaultTimeToLive = TimeSpan.FromDays(14);
// Agent with no TTL (never expires)
options.AddAIAgent(permanentAgent, timeToLive: null);
});
```
## How TTL works
The following sections describe how TTL works in detail.
### Expiration tracking
Each agent session maintains an expiration timestamp in its internally managed state that is updated whenever the session processes a message:
1. When a message is sent to an agent session, the expiration time is set to `current time + TTL`
2. The runtime schedules a delete operation for the expiration time (subject to minimum delay constraints)
3. When the delete operation runs, if the current time is past the expiration time, the session state is deleted. Otherwise, the delete operation is rescheduled for the next expiration time.
### State deletion
When an agent session expires, its entire state is deleted, including:
- Conversation history
- Any custom state data
- Expiration timestamps
After deletion, if a message is sent to the same agent session, a new session is created with a fresh conversation history.
## Behavior examples
The following examples illustrate how TTL works in different scenarios.
### Example 1: Agent session expires after TTL
1. Agent configured with 30-day TTL
2. User sends message at Day 0 → agent session created, expiration set to Day 30
3. No further messages sent
4. At Day 30 → Agent session is deleted
5. User sends message at Day 31 → New agent session created with fresh conversation history
### Example 2: TTL reset on interaction
1. Agent configured with 30-day TTL
2. User sends message at Day 0 → agent session created, expiration set to Day 30
3. User sends message at Day 15 → Expiration reset to Day 45
4. User sends message at Day 40 → Expiration reset to Day 70
5. Agent session remains active as long as there are regular interactions
## Logging
The TTL feature includes comprehensive logging to track state changes:
- **Expiration time updated**: Logged when TTL expiration time is set or updated
- **Deletion scheduled**: Logged when a deletion check signal is scheduled
- **Deletion check**: Logged when a deletion check operation runs
- **Session expired**: Logged when an agent session is deleted due to expiration
- **TTL rescheduled**: Logged when a deletion signal is rescheduled
These logs help monitor TTL behavior and troubleshoot any issues.
## Best practices
1. **Choose appropriate TTL values**: Balance between storage costs and user experience. Too short TTLs may delete active sessions, while too long TTLs may accumulate unnecessary state.
2. **Use per-agent TTLs**: Different agents may have different usage patterns. Configure TTLs per-agent based on expected session lifetimes.
3. **Monitor expiration logs**: Review logs to understand TTL behavior and adjust configuration as needed.
4. **Test with short TTLs**: During development, use short TTLs (e.g., minutes) to verify TTL behavior without waiting for long periods.
## Limitations
- TTL is based on wall-clock time, not activity time. The expiration timer starts from the last message timestamp.
- Deletion checks are durably scheduled operations and may have slight delays depending on system load.
- Once an agent session is deleted, its conversation history cannot be recovered.
- TTL deletion requires at least one worker to be available to process the deletion operation message.
@@ -172,7 +172,7 @@ parsing a structured payload into a typed record), without coupling the holder t
- Authorize and bind the candidate id to the authenticated principal/tenant before using it as an
`AgentSessionStore` key or a workflow checkpoint session id.
- For multi-user hosts, wrap the store with `IsolationKeyScopedAgentSessionStore` (for example via
`UseClaimsBasedSessionIsolation(...)`), so the session namespace is scoped per principal.
`UseClaimsBasedAgentIsolation(...)`), so the session namespace is scoped per principal.
- Persist session/checkpoint state only after the run or stream has completed.
## E2E Code Samples
+67 -9
View File
@@ -120,6 +120,10 @@ Code-reading landmarks:
- `_process_model_function_calls(...)` handles only calls from a completed model response.
- `_try_execute_function_calls(...)` decides approval/declaration/execution behavior for a batch.
- `_replace_approval_contents_with_results(...)` is the occurrence-aware approval transcript normalizer.
- `FunctionInvocationLayer._update_function_invocation_continuation_state(...)` updates continuation state after
every service response. Provider layers may override it to carry provider-specific continuation metadata into
the next service call, but must delegate to the base implementation so generic conversation continuation remains
synchronized with the active `AgentSession`.
### Approval pause and resume
@@ -331,11 +335,28 @@ that manually replay messages own the equivalent rule: do not resend an approval
- Service-managed continuation may omit inline reasoning/call items only when the hosted service already owns them.
- Missing non-reconstructable reasoning fails explicitly before a provider request instead of silently dropping the
content.
- Foundry clients do not request `reasoning.encrypted_content` implicitly; callers may opt in explicitly when the
selected deployment supports encrypted reasoning.
- Compaction preserves or excludes the complete reasoning/call/result group atomically.
### Approval request and resume
- A tool that requires approval does not execute before an approved response.
- With an `AgentSession`, every surfaced local or hosted approval request is stored as an immutable snapshot in one
active model batch. A new surfaced batch replaces an abandoned batch instead of accumulating session state.
- Approval request IDs use the provider function `call_id`, whose conversation-level uniqueness is required for
function-call/result correlation. Duplicate request IDs within one batch are rejected as malformed.
- An inbound response is honored only when its request id matches the pending server-held snapshot.
- Approval requests replayed in inbound message history do not create, replace, or resurrect approval authority.
- The executable call id, tool name, arguments, and local or hosted tool metadata are sourced from the recorded
request, never from the response payload.
- A matched approval response consumes its pending entry once. Unmatched, duplicate, and replayed responses do not
reach local execution.
- Tool lookup uses the recorded name against the current registry. A same-name implementation upgrade is allowed;
removing the name prevents local execution.
- Only the strict boolean `True` grants approval. Missing decisions and non-boolean values are rejection, not consent.
- Direct chat-client invocation without an `AgentSession` preserves pass-through compatibility, matching .NET;
authorization sinks still require strict `True`.
- An approved tool executes exactly once.
- A rejected tool executes zero times and produces one synthetic rejection `function_result` using the original
function `call_id`.
@@ -353,6 +374,20 @@ that manually replay messages own the equivalent rule: do not resend an approval
- `function_approval_request` and `function_approval_response` are control-plane contents, not durable model
transcript items.
- A current hosted approval response must be sent once on the immediate resume request.
- AG-UI removes a local approval response from its request and snapshot replay when a terminal result belongs to an
already-consumed occurrence, including result-before-response replay. A client-authored result in the occurrence
that is still registered as pending does not prove completion: AG-UI removes that result, keeps the validated
response for local execution, and leaves hosted approval responses as provider protocol data.
- Hosted AG-UI approval interrupts expose an accept/reject decision only; argument edits are rejected because the
hosted provider executes the server-owned request rather than client-edited arguments.
- AG-UI tool approval resumes accept the standard `approved` decision and full-replacement `editedArgs` payload.
Existing MAF clients remain compatible through the `accepted` decision alias and direct partial argument edits.
- An AG-UI `cancelled` resume is a valid terminal decision, not a run error. In a resume covering parallel open
interrupts, resolved siblings still execute and cancelled calls do not. An identical cancellation retry during
the retained terminal window also completes normally without restoring authority.
- AG-UI Approval State capacity is enforced independently for each trusted application scope. Abandoned pending
authority expires after its configured window, and indeterminate execution records remain non-retryable until
their separate safety window permits reclamation. Reclamation never recreates approval authority.
- A server-issued approval request must not be replayed inline during service-side continuation.
- History providers may retain approval control contents in their backing store for audit, but base history replay
filters them before later model calls.
@@ -364,7 +399,13 @@ that manually replay messages own the equivalent rule: do not resend an approval
- Model-bound history contains one function call/result pair per completed logical occurrence.
- Append-only history must not replay stale approval request/response wrappers to the model.
- Framework-managed and service-managed continuation must preserve the same logical call/result transcript.
- A terminal result consumes the corresponding approval authority in explicit stateless replay.
- A streaming response rebuilt from updates by an intermediate middleware must carry over the inner response's
conversation id and its internal-conversation-id marker, so framework-managed continuation appends only the latest
message instead of replaying a transcript the provider already holds. The rebuilt response mirrors the inner
conversation id exactly, including clearing it, and never retains an id emitted by an earlier service call in the
same turn.
- A trusted terminal result consumes the corresponding approval authority in explicit stateless replay; a result in a
server-registered pending occurrence cannot consume that authority before local execution.
## Scenario-to-test matrix
@@ -395,12 +436,17 @@ that manually replay messages own the equivalent rule: do not resend an approval
| Rejected streaming resume | Rejection result update precedes final text and tool executes zero times. | `test_approval_resume_returns_result_without_mutating_inputs[streaming-rejected]`, `test_streaming_approval_resume_yields_terminal_result_before_model_text[rejected]` |
| Mixed approved/rejected batch | Every call gets one correctly correlated terminal result. | `packages/core/tests/core/test_function_invocation_logic.py::test_rejected_approval` |
| Persisted approval replay | Resume executes with the prior call available. | `test_persisted_approval_messages_replay_correctly` |
| Hosted approval pass-through | Hosted requests/responses are not processed as local calls. | `test_hosted_tool_approval_response`, `test_hosted_mcp_approval_response_passthrough`, `test_mixed_local_and_hosted_approval_flow` |
| Hosted approval pass-through | Hosted requests/responses are bound to the recorded provider request and are not processed as local calls. | `test_hosted_tool_approval_response`, `test_hosted_mcp_approval_response_passthrough`, `test_session_approval_binding_reconstructs_hosted_response`, `test_mixed_local_and_hosted_approval_flow` |
| Approval-time user input | Every user-input request from one approved execution returns in order with assistant role and no extra model call; the execution consumes one call-budget unit. | `packages/core/tests/core/test_harness_tool_approval.py::test_approval_resume_returns_all_user_input_requests_without_another_model_call`, `packages/core/tests/core/test_function_invocation_logic.py::test_approval_resume_user_input_counts_toward_function_call_budget` |
| Mixed terminal result and follow-up input | Completed siblings remain tool-role while only follow-up input requests use assistant-role messages/updates. | `packages/core/tests/core/test_function_invocation_logic.py::test_approval_resume_separates_terminal_results_from_follow_up_requests`, `packages/openai/tests/openai/test_openai_chat_completion_client.py::test_mixed_approval_resume_roles_serialize_function_result_as_tool` |
| Approval-time middleware termination | Terminal result returns with no extra model call in either response mode. | `packages/core/tests/core/test_function_invocation_logic.py::test_approval_resume_honors_middleware_termination` |
| Approval re-entry after iteration budget | Pending approved calls resolve once even when prior model calls consumed `max_iterations`. | `packages/core/tests/core/test_harness_tool_approval.py::test_auto_approval_resolves_after_iteration_budget_is_exhausted` |
| Approval resume with reasoning | Model-bound resume history retains reasoning before the call and terminal result in both modes. | `packages/core/tests/core/test_harness_tool_approval.py::test_approval_resume_replays_reasoning_with_function_call_group` |
| Session-bound substituted response | A response is rebound to the immutable recorded call and cannot replace its call id, tool name, or arguments. | `packages/core/tests/core/test_function_invocation_logic.py::test_session_approval_binding_rebinds_consumes_and_rejects_duplicates` |
| Truthy non-boolean decision | Strings, integers, null, and other non-booleans do not authorize execution. | `packages/core/tests/core/test_function_invocation_logic.py::test_session_approval_binding_treats_truthy_non_boolean_as_rejection`, `packages/core/tests/core/test_types.py::test_function_approval_response_deserialization_rejects_non_boolean_decisions`, `packages/ag-ui/tests/ag_ui/test_message_adapters.py::test_function_approval_requires_real_boolean`, `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_resolve_approval_responses_treats_non_boolean_decision_as_rejection` |
| Active batch replacement | A newly surfaced model batch replaces abandoned approval authority instead of growing session state. | `packages/core/tests/core/test_function_invocation_logic.py::test_session_approval_binding_replaces_abandoned_batch` |
| Duplicate request id | Ambiguous request IDs within one active batch fail explicitly. | `packages/core/tests/core/test_function_invocation_logic.py::test_session_approval_batch_rejects_duplicate_request_ids` |
| Tool registry changes | Same-name upgrades may execute the recorded operation; removing the recorded name executes nothing. | `packages/core/tests/core/test_harness_tool_approval.py::test_approval_resume_allows_same_name_tool_upgrade`, `test_approval_resume_does_not_execute_when_recorded_tool_disappears` |
### Approval correlation and replay
@@ -419,6 +465,8 @@ that manually replay messages own the equivalent rule: do not resend an approval
| Missing result call id | A malformed result does not steal another approval's result. | `test_replace_approval_contents_with_results_skips_results_without_call_id` |
| Empty approval message cleanup | Fully consumed approval messages are removed from normalized model input. | `test_replace_approval_contents_with_results_prunes_emptied_messages` |
| Later stateless turn | A prior terminal approval response cannot execute again. | `test_resolved_approval_response_is_inert_on_later_stateless_turn` |
| Unbound or duplicate response | A response with no pending session request is removed; one request authorizes at most one response. | `test_session_approval_binding_rebinds_consumes_and_rejects_duplicates` |
| Forged inbound request history | A caller-supplied request wrapper cannot replace the server snapshot or resurrect consumed authority. | `test_session_approval_binding_does_not_trust_inbound_request_history` |
| Pending history turn | An unresolved approval batch is omitted atomically from unrelated model input while a later decision can still resume it once. | `packages/core/tests/core/test_harness_tool_approval.py::test_pending_approval_from_file_history_stays_resumable_without_model_orphan` |
| Duplicate function-call prevention | Approval normalization does not create a second call for one round. | `test_no_duplicate_function_calls_after_approval_processing` |
| Rejection call id | Rejection result uses the function call id, not only the approval id. | `test_rejection_result_uses_function_call_id` |
@@ -436,9 +484,16 @@ that manually replay messages own the equivalent rule: do not resend an approval
| Auto-approval callback | Callback receives the original function call and executes the approved set once. | `test_tool_approval_middleware_auto_approval_rule_receives_function_call` |
| Shared call budget | Auto-approved re-entry does not reset `max_function_calls`, and every executed approval group counts even when it pauses for input. | `test_tool_approval_middleware_auto_approved_loops_share_function_call_budget`, `test_approval_resume_user_input_counts_toward_function_call_budget` |
| Standing tool rule | Tool-level approval applies only to later matching tools. | `test_tool_approval_middleware_always_approve_tool_rule` |
| Forged standing rule | An unbound or substituted hosted response cannot create a standing middleware approval rule for caller-selected metadata. | `test_tool_approval_middleware_drops_forged_standing_approval`, `test_tool_approval_middleware_rebinds_hosted_standing_approval` |
| Hosted server boundary | Standing approval does not cross `server_label`. | `test_tool_approval_middleware_standing_rules_include_hosted_server_boundary` |
| Argument-scoped rule | Exact arguments are required; empty arguments are not tool-wide. | `test_tool_approval_middleware_always_approve_tool_with_arguments_rule`, `test_tool_approval_middleware_empty_arguments_rule_is_not_tool_wide` |
| Provider-injected approval tool | A tool added during `before_run` defers to in-run resolution, executes once, and emits one result. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_deferred_provider_tool_executes` |
| AG-UI provider boundary | Completed local approval controls from AG-UI request and snapshot replay are absent from raw chat-client input while deferred and hosted approvals keep their respective in-run/provider paths. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_does_not_forward_resolved_local_approval_control_to_chat_client`, `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_deferred_provider_tool_executes`, `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_canonical_resume_preserves_hosted_approval_for_provider`, `packages/ag-ui/tests/ag_ui/test_run.py::test_filter_local_approval_responses_for_provider_removes_duplicate_completed_controls`, `packages/ag-ui/tests/ag_ui/test_run.py::test_filter_local_approval_responses_for_provider_pairs_reused_call_ids_by_occurrence`, `packages/ag-ui/tests/ag_ui/test_run.py::test_canonical_hosted_approval_resume_rejects_edited_arguments_without_mutating_pending` |
| AG-UI standard approval payload | Agent and workflow tool approvals emit canonical `tool_call` interrupts. `approved` plus full-replacement `editedArgs` executes once and replays idempotently, while legacy `accepted` plus direct partial edits remains supported. Hosted approvals remain decision-only. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_entry_applies_standard_full_replacement_edited_args`, `test_endpoint_agent_approval_replayed_standard_edited_resume_is_idempotent`, `test_endpoint_agent_approval_resume_entry_applies_edited_arguments`, `test_workflow_endpoint_emits_canonical_tool_approval_interrupt`, `test_workflow_endpoint_accepts_canonical_tool_approval_resume`, `test_workflow_endpoint_applies_canonical_approval_edited_args`, `test_workflow_endpoint_accepts_legacy_partial_approval_edits`, `test_workflow_endpoint_hosted_approval_rejects_argument_edits` |
| AG-UI cancellation | A cancelled interrupt executes zero times and completes normally, including an identical retry during retained cancellation state; resolved siblings in the same complete resume still execute once. Workflow cancellation clears both runner correlation and the owning agent executor's pending request so later approvals remain resumable. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_cancelled_resume_entry_completes_without_execution`, `test_endpoint_agent_approval_replayed_cancellation_completes_idempotently`, `test_endpoint_agent_approval_mixed_cancelled_and_resolved_resume_executes_resolved_tool`, `test_endpoint_workflow_request_info_cancelled_resume_completes_normally`, `test_workflow_endpoint_cancelled_agent_approval_does_not_block_next_approval` |
| AG-UI approval retention and capacity | Pending authority expires automatically, indeterminate outcomes remain non-retryable until their safety window permits reclamation, and one trusted scope cannot consume another scope's occurrence quota. | `packages/ag-ui/tests/ag_ui/test_approval_lifecycle.py::test_abandoned_pending_occurrence_expires_and_releases_capacity`, `test_indeterminate_occurrence_is_reclaimed_after_its_safety_window`, `test_capacity_is_enforced_per_trusted_scope` |
| AG-UI local executor unavailable on resume | A claimed local occurrence whose executor disappeared releases its unstarted claim, reports temporary unavailability, and remains safely retryable. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_remains_retryable_when_local_tool_is_temporarily_unavailable` |
| AG-UI forwarded execution interruption | A provider failure, cancellation, or stream close after forwarding an approval recovers the open occurrence as indeterminate when no idempotency key proves retry safety. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_hosted_approval_becomes_indeterminate_when_provider_stream_fails` |
### Errors, control flow, and limits
@@ -465,18 +520,20 @@ that manually replay messages own the equivalent rule: do not resend an approval
| Pending hosted history replay | Stateless hosted approval requests remain replayable until a response is recorded, then both controls become inert. | `packages/openai/tests/openai/test_openai_chat_client.py::test_stateless_history_preserves_pending_hosted_approval_request_until_response` |
| Non-history provider plus session | Local history is still auto-injected for approval resume. | `packages/core/tests/core/test_agents.py::test_non_history_context_provider_still_injects_inmemory` |
| Hosted per-service-call persistence | A host-managed transcript remains available throughout a local function-call loop without being persisted into the framework session and replayed on the next hosted request. | `packages/foundry_hosting/tests/test_responses.py::TestAgentSessionPersistence::test_per_service_call_persistence_preserves_function_loop_history` |
| Service-side approval decision | Stored request is skipped; current approved or rejected response is sent. | `packages/openai/tests/openai/test_openai_chat_client.py::test_prepare_messages_strips_approval_request_but_keeps_response_under_storage` |
| OpenAI approval serialization | Approval id and decision serialize to `mcp_approval_response`. | `test_prepare_message_for_openai_with_function_approval_response`, `test_prepare_content_for_opentool_approval_response`, `test_function_approval_response_with_mcp_tool_call` |
| Streaming message injection with per-service-call persistence | A streaming response rebuilt from updates mirrors the inner conversation id exactly, including clearing it, and keeps its internal marker, so the next iteration appends only the latest message rather than replaying the whole turn on top of provider-held history, and never persists a conversation id from an earlier injected service call. | `packages/core/tests/core/test_middleware_with_chat.py::TestChatMiddleware::test_message_injection_middleware_streaming_preserves_inner_continuation_state`, `test_message_injection_middleware_streaming_keeps_service_conversation_id_external`, `test_message_injection_middleware_streaming_clears_conversation_id_when_final_call_has_none`, `test_message_injection_middleware_conversation_id_matches_across_streaming_modes`, `packages/core/tests/core/test_harness_agent.py::test_streaming_harness_tool_call_does_not_duplicate_transcript` |
| Service-side approval decision | Stored hosted request is skipped; the current approved or rejected hosted response is sent, while local approval controls are omitted from provider input. | `packages/openai/tests/openai/test_openai_chat_client.py::test_prepare_messages_strips_approval_request_but_keeps_response_under_storage`, `test_prepare_messages_drops_local_approval_controls` |
| OpenAI approval serialization | Hosted approval id and decision serialize to `mcp_approval_response`; local approvals remain in-process. | `test_prepare_message_for_openai_with_function_approval_response`, `test_prepare_content_for_opentool_approval_response`, `test_function_approval_response_with_mcp_tool_call` |
| OpenAI end-to-end hosted approval | Hosted request parses, response sends, and continuation completes. | `test_end_to_end_mcp_approval_flow` |
| Stored function call/result | Service-side storage drops server-issued calls but keeps new outputs. | `test_prepare_options_with_conversation_id_strips_server_issued_items`, `test_prepare_messages_for_openai_full_conversation_with_reasoning` |
| Stateless reasoning replay | Replay reconstructs reasoning, call, and result together; missing required reasoning fails before the request. | `test_tool_loop_store_false_replays_encrypted_reasoning_group`, `test_stateless_request_rejects_non_replayable_reasoning_bound_mcp_output`, `test_prepare_messages_for_openai_full_conversation_with_reasoning` |
| Foundry encrypted reasoning opt-in | Foundry clients omit `reasoning.encrypted_content` by default and preserve an explicit caller opt-in. | `packages/foundry/tests/foundry/test_foundry_chat_client.py::test_get_response_does_not_request_encrypted_reasoning_by_default`, `test_get_response_preserves_explicit_encrypted_reasoning_opt_in`, `packages/foundry/tests/foundry/test_foundry_agent.py::test_foundry_agent_basic_call_does_not_request_unsupported_encrypted_reasoning`, `test_foundry_agent_preserves_caller_requested_encrypted_reasoning`, `packages/foundry_hosting/tests/test_responses_int.py::TestReasoningHostedMcpReplay::test_second_turn_replays_mcp_call_with_encrypted_reasoning` |
| Opaque reasoning signature replay | Provider-specific opaque reasoning metadata is captured and restored on reconstructed calls. | `packages/gemini/tests/test_gemini_client.py::test_function_call_part_captures_thought_signature_as_reasoning_content`, `test_reconstructed_function_call_replays_thought_signature_from_reasoning_content` |
| Chat Completions approval wrappers | Framework approval wrappers are not sent as chat messages. | `packages/openai/tests/openai/test_openai_chat_completion_client.py` approval serialization tests |
| AG-UI approval result event | Approved result emits once with content and persists in snapshot. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_approval_resume_emits_tool_call_result`, `test_approval_resume_result_has_content`, `test_approval_resume_snapshot_replaces_approval_payload_with_tool_result`, `test_approval_resume_zero_updates_emits_tool_result` |
| AG-UI rejection/mixed decision | Transport emits only the events defined for approved and rejected calls without duplicates. | `test_rejection_does_not_emit_tool_call_result`, `test_mixed_approve_reject_emits_only_approved_tool_result`, `test_resolve_approval_responses_returns_only_approved` |
| AG-UI approval-time follow-up | The full grouped user-input pause remains in message history and emits no synthetic `TOOL_CALL_RESULT`. | `test_resolve_approval_responses_preserves_follow_up_user_input_group` |
| AG-UI approval execution failure | A grouped executor failure becomes one deterministic terminal error result for the approved call. | `test_resolve_approval_responses_returns_failure_when_grouped_execution_raises` |
| AG-UI no-approval path | Ordinary tool results do not gain an extra approval result event. | `test_no_approval_no_extra_tool_result` |
| AG-UI approval result event | Approved result emits once with content and persists in snapshot. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_approved_call_emits_one_live_result_under_original_identity`, `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_persists_replayable_tool_results`, `test_endpoint_agent_approval_replayed_resume_entry_reprojects_retained_result` |
| AG-UI rejection/mixed decision | Transport emits only the events defined for approved and rejected calls without duplicates. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_rejected_call_does_not_execute_or_emit_live_result`, `test_mixed_batch_preserves_approved_result_identity_and_order`, `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_rejection_releases_already_approved_sibling` |
| AG-UI approval-time follow-up | The full grouped user-input pause remains in message history and emits no synthetic `TOOL_CALL_RESULT`. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_approval_follow_up_group_remains_in_history_without_live_tool_result` |
| AG-UI approval execution failure | A grouped executor failure becomes one deterministic terminal error result for the approved call. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_approval_execution_failure_emits_one_terminal_error_result` |
| AG-UI no-approval path | Ordinary tool results do not gain an extra approval result event. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_no_approval_path_emits_no_approval_specific_duplicate_result` |
| AG-UI `confirm_changes` snapshot | An accepted synthetic confirmation is replaced only when its original function call has a real result; rejection is cleaned explicitly, and missing accepted results remain inert. | `packages/ag-ui/tests/ag_ui/test_confirm_changes_snapshot.py` |
| AG-UI malformed `confirm_changes` metadata | Non-list tool-call metadata and malformed argument JSON are ignored without guessing a target call. | `test_confirm_changes_target_ignores_non_list_tool_calls`, `test_confirm_changes_target_rejects_malformed_arguments_json` |
| Compaction pair integrity | Adjacent and non-adjacent pairs, including assistant-embedded results and completed reused-id occurrences, remain atomic without pairing ambiguous or out-of-order ids. | `packages/core/tests/core/test_compaction.py::test_group_annotations_keep_tool_call_and_tool_result_atomic`, `test_group_annotations_include_reasoning_in_tool_call_group`, `test_group_annotations_pair_nonadjacent_function_result_by_call_id`, `test_group_annotations_pair_multiple_nonadjacent_results_with_declaration`, `test_group_annotations_pair_completed_reused_call_id_occurrences`, `test_group_annotations_close_assistant_embedded_result_before_reused_call_id`, `test_sliding_window_does_not_retain_orphan_result_after_assistant_embedded_result`, `test_sliding_window_keeps_reused_call_id_occurrences_atomic`, `test_group_annotations_do_not_pair_ambiguous_duplicate_call_ids` |
@@ -510,6 +567,7 @@ uv run poe syntax -P openai
uv run poe pyright -P openai
uv run poe test-typing -P openai
uv run poe test -P ag-ui
uv run poe test -P declarative
uv run --directory packages/foundry_hosting poe test
```
+2 -1
View File
@@ -136,7 +136,8 @@ only to approved first-party endpoints.
| 15 | `core.in_memory_skills_source` | In-memory / programmatic skills | `agent_framework.InMemorySkillsSource` |
| 16 | `core.mcp_skills_source` | MCP-backed skills | `agent_framework.MCPSkillsSource` |
| 17 | `core.session_store` | Agent session store | `agent_framework.SessionStore` / `FileSessionStore` |
| 1831 | _reserved_ | core growth | — |
| 18 | `core.agent_hooks` | Agent Hooks middleware | `agent_framework.create_agent_hooks_middleware` |
| 1931 | _reserved_ | core growth | — |
| 32 | `orchestration.sequential` | Sequential orchestration | `agent_framework_orchestrations.SequentialBuilder` |
| 33 | `orchestration.concurrent` | Concurrent orchestration | `agent_framework_orchestrations.ConcurrentBuilder` |
| 34 | `orchestration.group_chat` | Group-chat orchestration | `agent_framework_orchestrations.GroupChatBuilder` |
+2 -17
View File
@@ -38,7 +38,7 @@
<PackageVersion Include="Google.GenAI" Version="1.6.0" />
<PackageVersion Include="Mscc.GenerativeAI.Microsoft" Version="2.9.3" />
<!-- Microsoft.Azure.* -->
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.54.0" />
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.61.0" />
<!-- Newtonsoft.Json -->
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<!-- System.* -->
@@ -104,6 +104,7 @@
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="10.7.0" />
<!-- Vector Stores -->
<PackageVersion Include="CommunityToolkit.VectorData.CosmosNoSql" Version="1.0.0" />
<PackageVersion Include="CommunityToolkit.VectorData.InMemory" Version="1.0.0" />
<PackageVersion Include="CommunityToolkit.VectorData.Qdrant" Version="1.0.0" />
<!-- Agent SDKs -->
@@ -134,22 +135,6 @@
<PackageVersion Include="Microsoft.Agents.ObjectModel.Json" Version="2026.2.4.1" />
<PackageVersion Include="Microsoft.Agents.ObjectModel.PowerFx" Version="2026.2.4.1" />
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.8.1" />
<!-- Durable Task -->
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.18.0" />
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.18.0" />
<PackageVersion Include="Microsoft.DurableTask.Worker" Version="1.18.0" />
<PackageVersion Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.18.0" />
<!-- Azure Functions -->
<PackageVersion Include="Microsoft.Azure.Functions.Worker" Version="2.50.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.12.1" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" Version="1.0.1" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.1.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Mcp" Version="1.0.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" />
<!-- Valkey -->
<!-- Redis -->
<PackageVersion Include="StackExchange.Redis" Version="2.10.1" />
<!-- Valkey -->
<PackageVersion Include="Valkey.Glide" Version="1.1.0" />
<!-- Console UX -->
+1 -49
View File
@@ -15,7 +15,6 @@
<Project Path="samples/01-get-started/03_multi_turn/03_multi_turn.csproj" />
<Project Path="samples/01-get-started/04_memory/04_memory.csproj" />
<Project Path="samples/01-get-started/05_first_workflow/05_first_workflow.csproj" />
<Project Path="samples/01-get-started/06_host_your_agent/06_host_your_agent.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/">
<File Path="samples/02-agents/README.md" />
@@ -73,24 +72,6 @@
<Folder Name="/Samples/02-agents/DeclarativeAgents/">
<Project Path="samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/DurableWorkflows/" />
<Folder Name="/Samples/04-hosting/DurableWorkflows/ConsoleApps/">
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/01_SequentialWorkflow/01_SequentialWorkflow.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/03_ConditionalEdges/03_ConditionalEdges.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/04_WorkflowAndAgents/04_WorkflowAndAgents.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/05_WorkflowEvents/05_WorkflowEvents.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/06_WorkflowSharedState/06_WorkflowSharedState.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/07_SubWorkflows/07_SubWorkflows.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/ConsoleApps/08_WorkflowHITL/08_WorkflowHITL.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/DurableWorkflows/AzureFunctions/">
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/01_SequentialWorkflow/01_SequentialWorkflow.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/03_WorkflowHITL/03_WorkflowHITL.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/04_WorkflowMcpTool/04_WorkflowMcpTool.csproj" />
<Project Path="samples/04-hosting/DurableWorkflows/AzureFunctions/05_WorkflowAndAgents/05_WorkflowAndAgents.csproj" />
</Folder>
<Folder Name="/Samples/GettingStarted/">
<File Path="samples/GettingStarted/README.md" />
</Folder>
@@ -206,6 +187,7 @@
<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" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step07_FileMemoryProvider/AgentWithMemory_Step07_FileMemoryProvider.csproj" />
<Project Path="samples/02-agents/AgentWithMemory/AgentWithMemory_Step08_MemoryUsingCosmosNoSql/AgentWithMemory_Step08_MemoryUsingCosmosNoSql.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/AgentProviders/openai/">
<File Path="samples/02-agents/AgentProviders/openai/README.md" />
@@ -401,29 +383,6 @@
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/HostedAgentSkills.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/DurableAgents/" />
<Folder Name="/Samples/04-hosting/DurableAgents/AzureFunctions/">
<File Path="samples/04-hosting/DurableAgents/AzureFunctions/.editorconfig" />
<File Path="samples/04-hosting/DurableAgents/AzureFunctions/README.md" />
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/01_SingleAgent/01_SingleAgent.csproj" />
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/06_LongRunningTools/06_LongRunningTools.csproj" />
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/07_AgentAsMcpTool/07_AgentAsMcpTool.csproj" />
<Project Path="samples/04-hosting/DurableAgents/AzureFunctions/08_ReliableStreaming/08_ReliableStreaming.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/DurableAgents/ConsoleApps/">
<File Path="samples/04-hosting/DurableAgents/ConsoleApps/README.md" />
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/01_SingleAgent/01_SingleAgent.csproj" />
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/02_AgentOrchestration_Chaining/02_AgentOrchestration_Chaining.csproj" />
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/03_AgentOrchestration_Concurrency/03_AgentOrchestration_Concurrency.csproj" />
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/04_AgentOrchestration_Conditionals/04_AgentOrchestration_Conditionals.csproj" />
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/05_AgentOrchestration_HITL/05_AgentOrchestration_HITL.csproj" />
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/06_LongRunningTools/06_LongRunningTools.csproj" />
<Project Path="samples/04-hosting/DurableAgents/ConsoleApps/07_ReliableStreaming/07_ReliableStreaming.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/A2A/">
<File Path="samples/02-agents/A2A/README.md" />
<Project Path="samples/02-agents/A2A/A2AAgent_AsFunctionTools/A2AAgent_AsFunctionTools.csproj" />
@@ -633,7 +592,6 @@
<Project Path="src/Microsoft.Agents.AI.CosmosNoSql/Microsoft.Agents.AI.CosmosNoSql.csproj" />
<Project Path="src/Microsoft.Agents.AI.Declarative/Microsoft.Agents.AI.Declarative.csproj" />
<Project Path="src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj" />
<Project Path="src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj" />
<Project Path="src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<Project Path="src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj" />
<Project Path="src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj" />
@@ -642,7 +600,6 @@
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AspNetCore/Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hyperlight/Microsoft.Agents.AI.Hyperlight.csproj" />
@@ -671,10 +628,8 @@
<Project Path="tests/Foundry.Hosting.IntegrationTests.TestContainer/Foundry.Hosting.IntegrationTests.TestContainer.csproj" />
<Project Path="tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj" />
<Project Path="tests/Foundry.IntegrationTests/Foundry.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hyperlight.IntegrationTests/Microsoft.Agents.AI.Hyperlight.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Mem0.IntegrationTests/Microsoft.Agents.AI.Mem0.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Tools.Shell.IntegrationTests/Microsoft.Agents.AI.Tools.Shell.IntegrationTests.csproj" />
@@ -692,14 +647,12 @@
<Project Path="tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Declarative.UnitTests/Microsoft.Agents.AI.Declarative.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Harness.UnitTests/Microsoft.Agents.AI.Harness.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.UnitTests/Microsoft.Agents.AI.Hosting.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hyperlight.UnitTests/Microsoft.Agents.AI.Hyperlight.UnitTests.csproj" />
@@ -718,4 +671,3 @@
</Folder>
</Solution>
-2
View File
@@ -14,13 +14,11 @@
"src\\Microsoft.Agents.AI.CosmosNoSql\\Microsoft.Agents.AI.CosmosNoSql.csproj",
"src\\Microsoft.Agents.AI.Declarative\\Microsoft.Agents.AI.Declarative.csproj",
"src\\Microsoft.Agents.AI.DevUI\\Microsoft.Agents.AI.DevUI.csproj",
"src\\Microsoft.Agents.AI.DurableTask\\Microsoft.Agents.AI.DurableTask.csproj",
"src\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj",
"src\\Microsoft.Agents.AI.Hosting.A2A\\Microsoft.Agents.AI.Hosting.A2A.csproj",
"src\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj",
"src\\Microsoft.Agents.AI.Hosting.AspNetCore\\Microsoft.Agents.AI.Hosting.AspNetCore.csproj",
"src\\Microsoft.Agents.AI.Hosting.AzureFunctions\\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj",
"src\\Microsoft.Agents.AI.Hosting.OpenAI\\Microsoft.Agents.AI.Hosting.OpenAI.csproj",
"src\\Microsoft.Agents.AI.Hosting\\Microsoft.Agents.AI.Hosting.csproj",
"src\\Microsoft.Agents.AI.LocalCodeAct\\Microsoft.Agents.AI.LocalCodeAct.csproj",
+3
View File
@@ -26,6 +26,9 @@
<ItemGroup Condition="'$(InjectSharedDiagnosticIds)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\DiagnosticIds\*.cs" LinkBase="Shared\DiagnosticIds" />
</ItemGroup>
<ItemGroup Condition="'$(InjectSharedUsage)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Usage\*.cs" LinkBase="Shared\Usage" />
</ItemGroup>
<ItemGroup Condition="'$(InjectSharedRedaction)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Redaction\*.cs" LinkBase="Shared\Redaction" />
</ItemGroup>
+3 -3
View File
@@ -26,7 +26,7 @@
When specified, only test projects whose filename matches this pattern are kept.
.PARAMETER TestProjectNameExcludeFilter
Optional wildcard pattern(s) to exclude test projects by name (e.g., *DurableTask.IntegrationTests*).
Optional wildcard pattern(s) to exclude test projects by name (e.g., *Slow.IntegrationTests*).
When specified, test projects whose filename matches any of these patterns are removed.
Applied after TestProjectNameIncludeFilter. Can be a single string or an array of strings.
@@ -50,8 +50,8 @@
dotnet test --solution (./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net472) --no-build -f net472
.EXAMPLE
# Generate integration tests excluding DurableTask and AzureFunctions
./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net10.0 -TestProjectNameIncludeFilter "*IntegrationTests*" -TestProjectNameExcludeFilter "*DurableTask.IntegrationTests*","*AzureFunctions.IntegrationTests*" -OutputPath filtered-other-integration.slnx
# Generate integration tests while excluding a long-running test project
./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net10.0 -TestProjectNameIncludeFilter "*IntegrationTests*" -TestProjectNameExcludeFilter "*Slow.IntegrationTests*" -OutputPath filtered-integration.slnx
#>
[CmdletBinding()]
@@ -532,6 +532,26 @@ internal static class AgentsSamples
],
},
new SampleDefinition
{
Name = "AgentWithMemory_Step08_MemoryUsingCosmosNoSql",
ProjectPath = "samples/02-agents/AgentWithMemory/AgentWithMemory_Step08_MemoryUsingCosmosNoSql",
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT", "COSMOS_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL", "FOUNDRY_EMBEDDING_MODEL", "COSMOS_DATABASE_NAME"],
MustContain =
[
"First session:",
"Second session (recalling prior chat history from Cosmos DB):",
],
ExpectedOutputDescription =
[
"The output should contain two joke responses.",
"The first joke should be about a pirate (as explicitly requested).",
"The second joke should also be pirate-themed or similar to what the user likes, since chat history from the first session should be recalled from Cosmos DB.",
"The output should not contain error messages or stack traces.",
],
},
// ── AgentWithRAG ────────────────────────────────────────────────────
new SampleDefinition
@@ -92,14 +92,5 @@ internal static class GetStartedSamples
"The output should not contain error messages or stack traces.",
],
},
new SampleDefinition
{
Name = "06_host_your_agent",
ProjectPath = "samples/01-get-started/06_host_your_agent",
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
SkipReason = "Requires Azure Functions Core Tools runtime and starts a web server.",
},
];
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.302",
"version": "10.0.303",
"rollForward": "minor",
"allowPrerelease": false
},
-6
View File
@@ -1,12 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>
+3 -3
View File
@@ -1,14 +1,14 @@
<Project>
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.16.0</VersionPrefix>
<VersionPrefix>1.18.0</VersionPrefix>
<RCNumber>1</RCNumber>
<DateSuffix>260730</DateSuffix>
<DateSuffix>260818</DateSuffix>
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).$(DateSuffix).1</PackageVersion>
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.$(DateSuffix).1</PackageVersion>
<PackageVersion Condition="'$(IsReleased)' == 'true'">$(VersionPrefix)</PackageVersion>
<GitTag>1.16.0</GitTag>
<GitTag>1.18.0</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
@@ -1,30 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>HostedAgent</AssemblyName>
<RootNamespace>HostedAgent</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -1,41 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to host an AI agent with Azure Functions (DurableAgents).
//
// Prerequisites:
// - Azure Functions Core Tools
// - Foundry project endpoint and credentials
//
// Environment variables:
// FOUNDRY_PROJECT_ENDPOINT
// FOUNDRY_MODEL (defaults to "gpt-5.4-mini")
//
// Run with: func start
// Then call: POST http://localhost:7071/api/agents/HostedAgent/run
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var model = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// Set up an AI agent following the standard Microsoft Agent Framework pattern.
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(model: model, instructions: "You are a helpful assistant hosted in Azure Functions.", name: "HostedAgent");
// Configure the function app to host the AI agent.
// This will automatically generate HTTP API endpoints for the agent.
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options => options.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1)))
.Build();
app.Run();
@@ -0,0 +1,3 @@
# Azure Functions Hosting Sample Has Moved
The Azure Functions hosting tutorial is now maintained as the [single-agent Durable Agent sample](https://github.com/microsoft/agent-framework-durable-extension/tree/main/dotnet/samples/DurableAgents/AzureFunctions/01_SingleAgent).
+1 -1
View File
@@ -228,7 +228,7 @@ dotnet run
`ConversationId` keeps request/response continuity. It is not proof that the caller owns that conversation. In multi-user deployments, authenticate each AG-UI request and authorize conversation access using your application's real boundary, such as the authenticated user, tenant, or workspace.
If your ASP.NET Core host shares session storage across users, pair `MapAGUI` with an isolation strategy such as `UseClaimsBasedSessionIsolation(...)` so the storage key includes a principal-specific dimension instead of relying on the conversation identifier alone.
If your ASP.NET Core host shares session storage across users, pair `MapAGUI` with an isolation strategy such as `UseClaimsBasedAgentIsolation(...)` so the storage key includes a principal-specific dimension instead of relying on the conversation identifier alone.
## Troubleshooting
@@ -11,9 +11,9 @@ builder.Services.AddHttpClient().AddLogging();
builder.Services.AddAGUIServer();
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
// deployments, e.g.:
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
WebApplication app = builder.Build();
@@ -17,9 +17,9 @@ builder.Services.ConfigureHttpJsonOptions(options =>
builder.Services.AddAGUIServer();
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
// deployments, e.g.:
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
WebApplication app = builder.Build();
@@ -11,9 +11,9 @@ builder.Services.AddHttpClient().AddLogging();
builder.Services.AddAGUIServer();
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
// deployments, e.g.:
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
WebApplication app = builder.Build();
@@ -28,9 +28,9 @@ builder.Services.ConfigureHttpJsonOptions(options =>
builder.Services.AddAGUIServer();
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
// deployments, e.g.:
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
WebApplication app = builder.Build();
@@ -18,9 +18,9 @@ builder.Services.AddAGUIServer();
builder.WebHost.UseUrls("http://localhost:8888");
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
// make sure to also register an AgentIsolationKeyProvider to scope sessions by principal in multi-user
// deployments, e.g.:
// builder.Services.UseClaimsBasedSessionIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
// builder.Services.UseClaimsBasedAgentIsolation(new() { ClaimType = ClaimTypes.NameIdentifier });
WebApplication app = builder.Build();
@@ -38,8 +38,8 @@
<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" />
<PackageReference Include="AgentMemory" Version="1.3.0" />
<PackageReference Include="AgentMemory.AgentFramework" Version="1.3.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" />
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="CommunityToolkit.VectorData.CosmosNoSql" />
<PackageReference Include="Microsoft.Azure.Cosmos" />
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample shows how to persist chat history in Azure Cosmos DB for NoSQL using the ChatHistoryMemoryProvider.
// The agent can then use chat history from prior conversations to inform responses in new conversations.
using System.Text.Json;
using Azure.AI.Projects;
using Azure.Identity;
using CommunityToolkit.VectorData.CosmosNoSql;
using Microsoft.Agents.AI;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
var embeddingDeploymentName = Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_MODEL") ?? "text-embedding-3-large";
var embeddingDimensions = 3072;
if (Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_DIMENSIONS") is string embeddingDimensionsValue &&
(!int.TryParse(embeddingDimensionsValue, out embeddingDimensions) || embeddingDimensions <= 0))
{
throw new InvalidOperationException("FOUNDRY_EMBEDDING_DIMENSIONS must be a positive integer.");
}
var cosmosEndpoint = Environment.GetEnvironmentVariable("COSMOS_ENDPOINT") ?? throw new InvalidOperationException("COSMOS_ENDPOINT is not set.");
var cosmosDatabaseName = Environment.GetEnvironmentVariable("COSMOS_DATABASE_NAME") ?? "agent-memory";
// 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.
DefaultAzureCredential credential = new();
AIProjectClient aiProjectClient = new(new Uri(endpoint), credential);
using CosmosClient cosmosClient = new(
cosmosEndpoint,
credential,
new CosmosClientOptions
{
UseSystemTextJsonSerializerWithOptions = JsonSerializerOptions.Default,
});
DatabaseResponse databaseResponse = await cosmosClient.CreateDatabaseIfNotExistsAsync(cosmosDatabaseName);
VectorStore vectorStore = new CosmosNoSqlVectorStore(
databaseResponse.Database,
new CosmosNoSqlVectorStoreOptions
{
JsonSerializerOptions = JsonSerializerOptions.Default,
EmbeddingGenerator = aiProjectClient
.GetProjectOpenAIClient()
.GetEmbeddingClient(embeddingDeploymentName)
.AsIEmbeddingGenerator(),
});
var userId = $"sample-{Guid.NewGuid():N}";
// Create the agent and add the ChatHistoryMemoryProvider to store chat messages in Cosmos DB.
AIAgent agent = aiProjectClient
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are good at telling jokes." },
Name = "Joker",
AIContextProviders = [new ChatHistoryMemoryProvider(
vectorStore,
collectionName: "chathistory",
vectorDimensions: embeddingDimensions,
// Callback to configure the initial state of the ChatHistoryMemoryProvider.
// The ChatHistoryMemoryProvider stores its state in the AgentSession and this callback
// will be called whenever the ChatHistoryMemoryProvider cannot find existing state in the session,
// typically the first time it is used with a new session.
_ => new ChatHistoryMemoryProvider.State(
// Configure the scope values under which chat messages will be stored.
// In this case, we are using a per-run user ID and a unique session ID for each new session.
storageScope: new() { UserId = userId, SessionId = Guid.NewGuid().ToString("N") },
// Configure the scope which would be used to search for relevant prior messages.
// In this case, we are searching for any messages for the user across all sessions.
searchScope: new() { UserId = userId }))]
});
// Start a new session for the agent conversation.
AgentSession session = await agent.CreateSessionAsync();
// Run the agent with the session that stores conversation history in Cosmos DB.
Console.WriteLine("First session:");
Console.WriteLine(await agent.RunAsync("I like jokes about Pirates. Tell me a joke about a pirate.", session));
// Start a second session. Since we configured the search scope to be across all sessions for the user,
// the agent should remember that the user likes pirate jokes.
AgentSession session2 = await agent.CreateSessionAsync();
// Run the agent with the second session.
Console.WriteLine("Second session (recalling prior chat history from Cosmos DB):");
Console.WriteLine(await agent.RunAsync("Tell me a joke that I might like.", session2));
@@ -0,0 +1,41 @@
# Agent with Memory Using Azure Cosmos DB for NoSQL
This sample uses `ChatHistoryMemoryProvider` with `CosmosNoSqlVectorStore` to persist chat history in Azure Cosmos DB for NoSQL and recall relevant messages in a new agent session.
## Features Demonstrated
- Authenticating to Microsoft Foundry and Azure Cosmos DB with `DefaultAzureCredential`
- Storing chat messages in an Azure Cosmos DB vector store
- Creating the configured database and chat-history container when they do not exist
- Recalling relevant chat history across agent sessions
## Prerequisites
1. [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
2. A Microsoft Foundry project with:
- A chat model deployment (the default is `gpt-5.4-mini`)
- A `text-embedding-3-large` deployment with 3,072 dimensions
3. An Azure Cosmos DB for NoSQL account with [vector search enabled](https://learn.microsoft.com/azure/cosmos-db/nosql/vector-search)
4. An Azure identity that can create the configured database and container and read and write items
5. Azure CLI authentication (`az login`)
## Configuration
Set the following environment variables:
| Variable | Description | Default |
|---|---|---|
| `FOUNDRY_PROJECT_ENDPOINT` | Microsoft Foundry project endpoint | *(required)* |
| `COSMOS_ENDPOINT` | Azure Cosmos DB account endpoint | *(required)* |
| `FOUNDRY_MODEL` | Chat model deployment name | `gpt-5.4-mini` |
| `FOUNDRY_EMBEDDING_MODEL` | Embedding model deployment name | `text-embedding-3-large` |
| `FOUNDRY_EMBEDDING_DIMENSIONS` | Number of dimensions produced by the embedding deployment | `3072` |
| `COSMOS_DATABASE_NAME` | Database used to store agent memory | `agent-memory` |
## Run the Sample
```bash
dotnet run
```
The first session stores the user's preference for pirate jokes. The second session uses a different `AgentSession` but the same per-run user search scope, allowing the agent to retrieve that preference from Azure Cosmos DB without recalling data from earlier sample runs.
@@ -11,6 +11,7 @@ These samples show how to create an agent with the Agent Framework that uses Mem
|[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.|
|[File Based Memory](./AgentWithMemory_Step07_FileMemoryProvider/)|This sample demonstrates how to use the `FileMemoryProvider` to give an agent tools for storing and recalling memories as files, and how to configure the folder that those memory files are written to.|
|[Memory with Azure Cosmos DB for NoSQL](./AgentWithMemory_Step08_MemoryUsingCosmosNoSql/)|This sample demonstrates how to persist and retrieve chat history across sessions with Azure Cosmos DB for NoSQL.|
> **See also**: [Memory Search with Foundry Agents](../AgentProviders/foundry/Agent_Step22_MemorySearch/) - demonstrates using the built-in Memory Search tool with Microsoft Foundry agents.
@@ -37,7 +37,7 @@ TextSearchStore textSearchStore = new(vectorStore, "product-and-policy-info", 30
await textSearchStore.UpsertDocumentsAsync(GetSampleDocuments());
// Create an adapter function that the TextSearchProvider can use to run searches against the TextSearchStore.
Func<string, CancellationToken, Task<IEnumerable<TextSearchProvider.TextSearchResult>>> SearchAdapter = async (text, ct) =>
async Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchAdapterAsync(string text, CancellationToken ct)
{
// Here we are limiting the search results to the single top result to demonstrate that we are accurately matching
// specific search results for each question, but in a real world case, more results should be used.
@@ -49,7 +49,7 @@ Func<string, CancellationToken, Task<IEnumerable<TextSearchProvider.TextSearchRe
Text = r.Text ?? string.Empty,
RawRepresentation = r
});
};
}
// Configure the options for the TextSearchProvider.
TextSearchProviderOptions textSearchOptions = new()
@@ -63,7 +63,7 @@ AIAgent agent = aiProjectClient
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
AIContextProviders = [new TextSearchProvider(SearchAdapter, textSearchOptions)],
AIContextProviders = [new TextSearchProvider(SearchAdapterAsync, textSearchOptions)],
// Since we are using ChatCompletion which stores chat history locally, we can also add a message filter
// that removes messages produced by the TextSearchProvider before they are added to the chat history, so that
// we don't bloat chat history with all the search result messages.
@@ -40,7 +40,7 @@ await UploadDataFromMarkdown(afOverviewUrl, "Microsoft Agent Framework Overview"
await UploadDataFromMarkdown(afMigrationUrl, "Semantic Kernel to Microsoft Agent Framework Migration Guide", documentationCollection, 2000, 200);
// Create an adapter function that the TextSearchProvider can use to run searches against the collection.
Func<string, CancellationToken, Task<IEnumerable<TextSearchProvider.TextSearchResult>>> SearchAdapter = async (text, ct) =>
async Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchAdapterAsync(string text, CancellationToken ct)
{
List<TextSearchProvider.TextSearchResult> results = [];
await foreach (var result in documentationCollection.SearchAsync(text, 5, cancellationToken: ct))
@@ -54,7 +54,7 @@ Func<string, CancellationToken, Task<IEnumerable<TextSearchProvider.TextSearchRe
});
}
return results;
};
}
// Configure the options for the TextSearchProvider.
TextSearchProviderOptions textSearchOptions = new()
@@ -72,7 +72,7 @@ AIAgent agent = aiProjectClient
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief." },
AIContextProviders = [new TextSearchProvider(SearchAdapter, textSearchOptions)],
AIContextProviders = [new TextSearchProvider(SearchAdapterAsync, textSearchOptions)],
// Configure a filter on the InMemoryChatHistoryProvider so that we don't persist the messages produced by the TextSearchProvider in chat history.
// The default is to persist all messages except those that came from chat history in the first place.
// You may choose to persist the TextSearchProvider messages, if you want the search output to be provided to the model in future interactions as well.
@@ -23,7 +23,7 @@ var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ??
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// A sample function to load the next three calendar events for the user.
Func<Task<string[]>> loadNextThreeCalendarEvents = async () =>
async Task<string[]> LoadNextThreeCalendarEventsAsync()
{
// In a real implementation, this method would connect to a calendar service
return
@@ -32,7 +32,7 @@ Func<Task<string[]>> loadNextThreeCalendarEvents = async () =>
"Team meeting today at 17:00",
"Birthday party today at 20:00"
];
};
}
// Create an agent with an AI context provider attached that aggregates two other providers.
// You must dissable client side conversation storage for clients that support it:
@@ -64,7 +64,7 @@ AIAgent agent = new AIProjectClient(
// The agent will call each provider in sequence, accumulating context from each.
AIContextProviders = [
new TodoListAIContextProvider(),
new CalendarSearchAIContextProvider(loadNextThreeCalendarEvents)
new CalendarSearchAIContextProvider(LoadNextThreeCalendarEventsAsync)
],
});
@@ -123,20 +123,27 @@ internal sealed class Program
private static DeclarativeAgentDefinition DefinePlannerAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions = // TODO: Use Structured Inputs / Prompt Template
Instructions =
"""
Your only job is to devise an efficient plan that identifies (by name) how a team member may contribute to addressing the user request.
Only select the following team which is listed as "- [Name]: [Description]"
- WeatherAgent: Able to retrieve weather information
- CoderAgent: Able to write and execute Python code
- KnowledgeAgent: Able to perform generic websearches
{{team}}
The plan must be a bullet point list must be in the form "- [AgentName]: [Specific action or task for that agent to perform]"
Remember, there is no requirement to involve the entire team -- only select team member's whose particular expertise is required for this task.
"""
""",
StructuredInputs =
{
["team"] =
new StructuredInputDefinition
{
IsRequired = true,
Description = "The available team members and their capabilities.",
}
}
};
private static DeclarativeAgentDefinition DefineManagerAgent(IConfiguration configuration) =>
@@ -151,7 +158,7 @@ internal sealed class Program
- WeatherAgent: Able to retrieve weather information
To make progress on the request, please answer the following questions, including necessary reasoning:
- Is the request fully satisfied? (True if complete, or False if the original request has yet to be SUCCESSFULLY and FULLY addressed)
- Is the request fully satisfied? (True if the requested work is complete and enough information is available to provide the final answer. A verified negative or empty result, such as confirming that a requested resource does not exist, can fully satisfy the request. False if work remains or if a negative or empty result may be caused by an execution, authorization, connectivity, or investigation failure.)
- Are we in a loop where we are repeating the same requests and / or getting the same responses from an agent multiple times? Loops can span multiple turns, and can include repeated actions like scrolling up or down more than a handful of times.
- Are we making forward progress? (True if just starting, or recent messages are adding value. False if recent messages show evidence of being stuck in a loop or if there is evidence of significant barriers to success such as the inability to read from a required file)
- Who should speak next? (select from: KnowledgeAgent, CoderAgent, WeatherAgent)
@@ -59,6 +59,10 @@ static async Task RunWorkflowAsync(Workflow workflow)
}
await run.TrySendMessageAsync(userInput);
// Agents are wrapped as executors that cache incoming messages and only run when they receive a TurnToken,
// so the turn must be triggered explicitly after sending the user input.
await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
string? speakingAgent = null;
await foreach (WorkflowEvent evt in run.WatchStreamAsync(cts.Token))
{
@@ -1,10 +0,0 @@
# .editorconfig
[*.cs]
# See https://github.com/Azure/azure-functions-durable-extension/issues/3173
dotnet_diagnostic.DURABLE0001.severity = none
dotnet_diagnostic.DURABLE0002.severity = none
dotnet_diagnostic.DURABLE0003.severity = none
dotnet_diagnostic.DURABLE0004.severity = none
dotnet_diagnostic.DURABLE0005.severity = none
dotnet_diagnostic.DURABLE0006.severity = none
@@ -1,42 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>SingleAgent</AssemblyName>
<RootNamespace>SingleAgent</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -1,42 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable IDE0002 // Simplify Member Access
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI.Chat;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
// Set up an AI agent following the standard Microsoft Agent Framework pattern.
const string JokerName = "Joker";
const string JokerInstructions = "You are good at telling jokes.";
AIAgent agent = client.GetChatClient(deploymentName).AsAIAgent(JokerInstructions, JokerName);
// Configure the function app to host the AI agent.
// This will automatically generate HTTP API endpoints for the agent.
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options => options.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1)))
.Build();
app.Run();
@@ -1,89 +0,0 @@
# Single Agent Sample
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that hosts a single AI agent and provides direct HTTP API access for interactive conversations.
## Key Concepts Demonstrated
- Using the Microsoft Agent Framework to define a simple AI agent with a name and instructions.
- Registering agents with the Function app and running them using HTTP.
- Conversation management (via session IDs) for isolated interactions.
## Environment Setup
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
## Running the Sample
With the environment setup and function app running, you can test the sample by sending an HTTP request to the agent endpoint.
You can use the `demo.http` file to send a message to the agent, or a command line tool like `curl` as shown below:
Bash (Linux/macOS/WSL):
```bash
curl -X POST http://localhost:7071/api/agents/Joker/run \
-H "Content-Type: text/plain" \
-d "Tell me a joke about a pirate."
```
PowerShell:
```powershell
Invoke-RestMethod -Method Post `
-Uri http://localhost:7071/api/agents/Joker/run `
-ContentType text/plain `
-Body "Tell me a joke about a pirate."
```
You can also send JSON requests:
```bash
curl -X POST http://localhost:7071/api/agents/Joker/run \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"message": "Tell me a joke about a pirate."}'
```
To continue a conversation, include the `thread_id` in the query string or JSON body:
```bash
curl -X POST "http://localhost:7071/api/agents/Joker/run?thread_id=your-thread-id" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"message": "Tell me another one."}'
```
The response from the agent will be displayed in the terminal where you ran `func start`. The expected `text/plain` output will look something like:
```text
Why don't pirates ever learn the alphabet? Because they always get stuck at "C"!
```
The expected `application/json` output will look something like:
```json
{
"status": 200,
"thread_id": "ee6e47a0-f24b-40b1-ade8-16fcebb9eb40",
"response": {
"Messages": [
{
"AuthorName": "Joker",
"CreatedAt": "2025-11-11T12:00:00.0000000Z",
"Role": "assistant",
"Contents": [
{
"Type": "text",
"Text": "Why don't pirates ever learn the alphabet? Because they always get stuck at 'C'!"
}
]
}
],
"Usage": {
"InputTokenCount": 78,
"OutputTokenCount": 36,
"TotalTokenCount": 114
}
}
}
```
@@ -1,8 +0,0 @@
# Default endpoint address for local testing
@authority=http://localhost:7071
### Prompt the agent
POST {{authority}}/api/agents/Joker/run
Content-Type: text/plain
Tell me a joke about a pirate.
@@ -1,20 +0,0 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Agents.AI.DurableTask": "Information",
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -1,42 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>AgentOrchestration_Chaining</AssemblyName>
<RootNamespace>AgentOrchestration_Chaining</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -1,92 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
namespace AgentOrchestration_Chaining;
public static class FunctionTriggers
{
public sealed record TextResponse(string Text);
[Function(nameof(RunOrchestrationAsync))]
public static async Task<string> RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
{
DurableAIAgent writer = context.GetAgent("WriterAgent");
AgentSession writerSession = await writer.CreateSessionAsync();
AgentResponse<TextResponse> initial = await writer.RunAsync<TextResponse>(
message: "Write a concise inspirational sentence about learning.",
session: writerSession);
AgentResponse<TextResponse> refined = await writer.RunAsync<TextResponse>(
message: $"Improve this further while keeping it under 25 words: {initial.Result.Text}",
session: writerSession);
return refined.Result.Text;
}
// POST /singleagent/run
[Function(nameof(StartOrchestrationAsync))]
public static async Task<HttpResponseData> StartOrchestrationAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "singleagent/run")] HttpRequestData req,
[DurableClient] DurableTaskClient client)
{
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
orchestratorName: nameof(RunOrchestrationAsync));
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
await response.WriteAsJsonAsync(new
{
message = "Single-agent orchestration started.",
instanceId,
statusQueryGetUri = GetStatusQueryGetUri(req, instanceId),
});
return response;
}
// GET /singleagent/status/{instanceId}
[Function(nameof(GetOrchestrationStatusAsync))]
public static async Task<HttpResponseData> GetOrchestrationStatusAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "singleagent/status/{instanceId}")] HttpRequestData req,
string instanceId,
[DurableClient] DurableTaskClient client)
{
OrchestrationMetadata? status = await client.GetInstanceAsync(
instanceId,
getInputsAndOutputs: true,
req.FunctionContext.CancellationToken);
if (status is null)
{
HttpResponseData notFound = req.CreateResponse(HttpStatusCode.NotFound);
await notFound.WriteAsJsonAsync(new { error = "Instance not found" });
return notFound;
}
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new
{
instanceId = status.InstanceId,
runtimeStatus = status.RuntimeStatus.ToString(),
input = status.SerializedInput is not null ? (object)status.ReadInputAs<JsonElement>() : null,
output = status.SerializedOutput is not null ? (object)status.ReadOutputAs<JsonElement>() : null,
failureDetails = status.FailureDetails
});
return response;
}
private static string GetStatusQueryGetUri(HttpRequestData req, string instanceId)
{
// NOTE: This can be made more robust by considering the value of
// request headers like "X-Forwarded-Host" and "X-Forwarded-Proto".
string authority = $"{req.Url.Scheme}://{req.Url.Authority}";
return $"{authority}/api/singleagent/status/{instanceId}";
}
}
@@ -1,45 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable IDE0002 // Simplify Member Access
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI.Chat;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
// Single agent used by the orchestration to demonstrate sequential calls on the same session.
const string WriterName = "WriterAgent";
const string WriterInstructions =
"""
You refine short pieces of text. When given an initial sentence you enhance it;
when given an improved sentence you polish it further.
""";
AIAgent writerAgent = client.GetChatClient(deploymentName).AsAIAgent(WriterInstructions, WriterName);
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options => options.AddAIAgent(writerAgent))
.Build();
app.Run();
@@ -1,59 +0,0 @@
# Single Agent Orchestration Sample
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that orchestrates sequential calls to a single AI agent using the same session for context continuity.
## Key Concepts Demonstrated
- Orchestrating multiple interactions with the same agent in a deterministic order
- Using the same `AgentSession` across multiple calls to maintain conversational context
- Durable orchestration with automatic checkpointing and resumption from failures
- HTTP API integration for starting and monitoring orchestrations
## Environment Setup
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
## Running the Sample
With the environment setup and function app running, you can test the sample by sending an HTTP request to start the orchestration.
You can use the `demo.http` file to start the orchestration, or a command line tool like `curl` as shown below:
Bash (Linux/macOS/WSL):
```bash
curl -X POST http://localhost:7071/api/singleagent/run
```
PowerShell:
```powershell
Invoke-RestMethod -Method Post -Uri http://localhost:7071/api/singleagent/run
```
The response will be a JSON object that looks something like the following, which indicates that the orchestration has started.
```json
{
"message": "Single-agent orchestration started.",
"instanceId": "86313f1d45fb42eeb50b1852626bf3ff",
"statusQueryGetUri": "http://localhost:7071/api/singleagent/status/86313f1d45fb42eeb50b1852626bf3ff"
}
```
The orchestration will proceed to run the WriterAgent twice in sequence:
1. First, it writes an inspirational sentence about learning
2. Then, it refines the initial output using the same conversation thread
Once the orchestration has completed, you can get the status of the orchestration by sending a GET request to the `statusQueryGetUri` URL. The response will be a JSON object that looks something like the following:
```json
{
"failureDetails": null,
"input": null,
"instanceId": "86313f1d45fb42eeb50b1852626bf3ff",
"output": "Learning serves as the key, opening doors to boundless opportunities and a brighter future.",
"runtimeStatus": "Completed"
}
```
@@ -1,3 +0,0 @@
### Start the single-agent orchestration
POST http://localhost:7071/api/singleagent/run
@@ -1,20 +0,0 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Agents.AI.DurableTask": "Information",
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -1,42 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>AgentOrchestration_Concurrency</AssemblyName>
<RootNamespace>AgentOrchestration_Concurrency</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -1,116 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
namespace AgentOrchestration_Concurrency;
public static class FunctionsTriggers
{
public sealed record TextResponse(string Text);
[Function(nameof(RunOrchestrationAsync))]
public static async Task<object> RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
{
// Get the prompt from the orchestration input
string prompt = context.GetInput<string>() ?? throw new InvalidOperationException("Prompt is required");
// Get both agents
DurableAIAgent physicist = context.GetAgent("PhysicistAgent");
DurableAIAgent chemist = context.GetAgent("ChemistAgent");
// Start both agent runs concurrently
Task<AgentResponse<TextResponse>> physicistTask = physicist.RunAsync<TextResponse>(prompt);
Task<AgentResponse<TextResponse>> chemistTask = chemist.RunAsync<TextResponse>(prompt);
// Wait for both tasks to complete using Task.WhenAll
await Task.WhenAll(physicistTask, chemistTask);
// Get the results
TextResponse physicistResponse = (await physicistTask).Result;
TextResponse chemistResponse = (await chemistTask).Result;
// Return the result as a structured, anonymous type
return new
{
physicist = physicistResponse.Text,
chemist = chemistResponse.Text,
};
}
// POST /multiagent/run
[Function(nameof(StartOrchestrationAsync))]
public static async Task<HttpResponseData> StartOrchestrationAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "multiagent/run")] HttpRequestData req,
[DurableClient] DurableTaskClient client)
{
// Read the prompt from the request body
string? prompt = await req.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(prompt))
{
HttpResponseData badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await badRequestResponse.WriteAsJsonAsync(new { error = "Prompt is required" });
return badRequestResponse;
}
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
orchestratorName: nameof(RunOrchestrationAsync),
input: prompt);
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
await response.WriteAsJsonAsync(new
{
message = "Multi-agent concurrent orchestration started.",
prompt,
instanceId,
statusQueryGetUri = GetStatusQueryGetUri(req, instanceId),
});
return response;
}
// GET /multiagent/status/{instanceId}
[Function(nameof(GetOrchestrationStatusAsync))]
public static async Task<HttpResponseData> GetOrchestrationStatusAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "multiagent/status/{instanceId}")] HttpRequestData req,
string instanceId,
[DurableClient] DurableTaskClient client)
{
OrchestrationMetadata? status = await client.GetInstanceAsync(
instanceId,
getInputsAndOutputs: true,
req.FunctionContext.CancellationToken);
if (status is null)
{
HttpResponseData notFound = req.CreateResponse(HttpStatusCode.NotFound);
await notFound.WriteAsJsonAsync(new { error = "Instance not found" });
return notFound;
}
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new
{
instanceId = status.InstanceId,
runtimeStatus = status.RuntimeStatus.ToString(),
input = status.SerializedInput is not null ? (object)status.ReadInputAs<JsonElement>() : null,
output = status.SerializedOutput is not null ? (object)status.ReadOutputAs<JsonElement>() : null,
failureDetails = status.FailureDetails
});
return response;
}
private static string GetStatusQueryGetUri(HttpRequestData req, string instanceId)
{
// NOTE: This can be made more robust by considering the value of
// request headers like "X-Forwarded-Host" and "X-Forwarded-Proto".
string authority = $"{req.Url.Scheme}://{req.Url.Authority}";
return $"{authority}/api/multiagent/status/{instanceId}";
}
}
@@ -1,50 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable IDE0002 // Simplify Member Access
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI.Chat;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
// Two agents used by the orchestration to demonstrate concurrent execution.
const string PhysicistName = "PhysicistAgent";
const string PhysicistInstructions = "You are an expert in physics. You answer questions from a physics perspective.";
const string ChemistName = "ChemistAgent";
const string ChemistInstructions = "You are an expert in chemistry. You answer questions from a chemistry perspective.";
AIAgent physicistAgent = client.GetChatClient(deploymentName).AsAIAgent(PhysicistInstructions, PhysicistName);
AIAgent chemistAgent = client.GetChatClient(deploymentName).AsAIAgent(ChemistInstructions, ChemistName);
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options =>
{
options
.AddAIAgent(physicistAgent)
.AddAIAgent(chemistAgent);
})
.Build();
app.Run();
@@ -1,65 +0,0 @@
# Multi-Agent Concurrent Orchestration Sample
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create an Azure Functions app that orchestrates concurrent execution of multiple AI agents, each with specialized expertise, to provide comprehensive answers to complex questions.
## Key Concepts Demonstrated
- Multi-agent orchestration with specialized AI agents (physics and chemistry)
- Concurrent execution using the fan-out/fan-in pattern for improved performance and distributed processing
- Response aggregation from multiple agents into a unified result
- Durable orchestration with automatic checkpointing and resumption from failures
## Environment Setup
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
## Running the Sample
With the environment setup and function app running, you can test the sample by sending an HTTP request with a custom prompt to the orchestration.
You can use the `demo.http` file to send a message to the agents, or a command line tool like `curl` as shown below:
Bash (Linux/macOS/WSL):
```bash
curl -X POST http://localhost:7071/api/multiagent/run \
-H "Content-Type: text/plain" \
-d "What is temperature?"
```
PowerShell:
```powershell
Invoke-RestMethod -Method Post `
-Uri http://localhost:7071/api/multiagent/run `
-ContentType text/plain `
-Body "What is temperature?"
```
The response will be a JSON object that looks something like the following, which indicates that the orchestration has started.
```json
{
"message": "Multi-agent concurrent orchestration started.",
"prompt": "What is temperature?",
"instanceId": "e7e29999b6b8424682b3539292afc9ed",
"statusQueryGetUri": "http://localhost:7071/api/multiagent/status/e7e29999b6b8424682b3539292afc9ed"
}
```
The orchestration will run both the PhysicistAgent and ChemistAgent concurrently, asking them the same question. Their responses will be combined to provide a comprehensive answer covering both physical and chemical aspects.
Once the orchestration has completed, you can get the status of the orchestration by sending a GET request to the `statusQueryGetUri` URL. The response will be a JSON object that looks something like the following:
```json
{
"failureDetails": null,
"input": "What is temperature?",
"instanceId": "e7e29999b6b8424682b3539292afc9ed",
"output": {
"physicist": "Temperature is a measure of the average kinetic energy of particles in a system. From a physics perspective, it represents the thermal energy and determines the direction of heat flow between objects.",
"chemist": "From a chemistry perspective, temperature is crucial for chemical reactions as it affects reaction rates through the Arrhenius equation. It influences the equilibrium position of reversible reactions and determines the physical state of substances."
},
"runtimeStatus": "Completed"
}
```
@@ -1,5 +0,0 @@
### Start the multi-agent concurrent orchestration
POST http://localhost:7071/api/multiagent/run
Content-Type: text/plain
What is temperature?
@@ -1,20 +0,0 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Agents.AI.DurableTask": "Information",
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -1,42 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>AgentOrchestration_Conditionals</AssemblyName>
<RootNamespace>AgentOrchestration_Conditionals</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -1,143 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
namespace AgentOrchestration_Conditionals;
public static class FunctionTriggers
{
[Function(nameof(RunOrchestrationAsync))]
public static async Task<string> RunOrchestrationAsync([OrchestrationTrigger] TaskOrchestrationContext context)
{
// Get the email from the orchestration input
Email email = context.GetInput<Email>() ?? throw new InvalidOperationException("Email is required");
// Get the spam detection agent
DurableAIAgent spamDetectionAgent = context.GetAgent("SpamDetectionAgent");
AgentSession spamSession = await spamDetectionAgent.CreateSessionAsync();
// Step 1: Check if the email is spam
AgentResponse<DetectionResult> spamDetectionResponse = await spamDetectionAgent.RunAsync<DetectionResult>(
message:
$"""
Analyze this email for spam content and return a JSON response with 'is_spam' (boolean) and 'reason' (string) fields:
Email ID: {email.EmailId}
Content: {email.EmailContent}
""",
session: spamSession);
DetectionResult result = spamDetectionResponse.Result;
// Step 2: Conditional logic based on spam detection result
if (result.IsSpam)
{
// Handle spam email
return await context.CallActivityAsync<string>(nameof(HandleSpamEmail), result.Reason);
}
// Generate and send response for legitimate email
DurableAIAgent emailAssistantAgent = context.GetAgent("EmailAssistantAgent");
AgentSession emailSession = await emailAssistantAgent.CreateSessionAsync();
AgentResponse<EmailResponse> emailAssistantResponse = await emailAssistantAgent.RunAsync<EmailResponse>(
message:
$"""
Draft a professional response to this email. Return a JSON response with a 'response' field containing the reply:
Email ID: {email.EmailId}
Content: {email.EmailContent}
""",
session: emailSession);
EmailResponse emailResponse = emailAssistantResponse.Result;
return await context.CallActivityAsync<string>(nameof(SendEmail), emailResponse.Response);
}
[Function(nameof(HandleSpamEmail))]
public static string HandleSpamEmail([ActivityTrigger] string reason)
{
return $"Email marked as spam: {reason}";
}
[Function(nameof(SendEmail))]
public static string SendEmail([ActivityTrigger] string message)
{
return $"Email sent: {message}";
}
// POST /spamdetection/run
[Function(nameof(StartOrchestrationAsync))]
public static async Task<HttpResponseData> StartOrchestrationAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "spamdetection/run")] HttpRequestData req,
[DurableClient] DurableTaskClient client)
{
// Read the email from the request body
Email? email = await req.ReadFromJsonAsync<Email>();
if (email is null || string.IsNullOrWhiteSpace(email.EmailContent))
{
HttpResponseData badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await badRequestResponse.WriteAsJsonAsync(new { error = "Email with content is required" });
return badRequestResponse;
}
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
orchestratorName: nameof(RunOrchestrationAsync),
input: email);
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
await response.WriteAsJsonAsync(new
{
message = "Spam detection orchestration started.",
emailId = email.EmailId,
instanceId,
statusQueryGetUri = GetStatusQueryGetUri(req, instanceId),
});
return response;
}
// GET /spamdetection/status/{instanceId}
[Function(nameof(GetOrchestrationStatusAsync))]
public static async Task<HttpResponseData> GetOrchestrationStatusAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "spamdetection/status/{instanceId}")] HttpRequestData req,
string instanceId,
[DurableClient] DurableTaskClient client)
{
OrchestrationMetadata? status = await client.GetInstanceAsync(
instanceId,
getInputsAndOutputs: true,
req.FunctionContext.CancellationToken);
if (status is null)
{
HttpResponseData notFound = req.CreateResponse(HttpStatusCode.NotFound);
await notFound.WriteAsJsonAsync(new { error = "Instance not found" });
return notFound;
}
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new
{
instanceId = status.InstanceId,
runtimeStatus = status.RuntimeStatus.ToString(),
input = status.SerializedInput is not null ? (object)status.ReadInputAs<JsonElement>() : null,
output = status.SerializedOutput is not null ? (object)status.ReadOutputAs<JsonElement>() : null,
failureDetails = status.FailureDetails
});
return response;
}
private static string GetStatusQueryGetUri(HttpRequestData req, string instanceId)
{
// NOTE: This can be made more robust by considering the value of
// request headers like "X-Forwarded-Host" and "X-Forwarded-Proto".
string authority = $"{req.Url.Scheme}://{req.Url.Authority}";
return $"{authority}/api/spamdetection/status/{instanceId}";
}
}
@@ -1,38 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AgentOrchestration_Conditionals;
/// <summary>
/// Represents an email input for spam detection and response generation.
/// </summary>
public sealed class Email
{
[JsonPropertyName("email_id")]
public string EmailId { get; set; } = string.Empty;
[JsonPropertyName("email_content")]
public string EmailContent { get; set; } = string.Empty;
}
/// <summary>
/// Represents the result of spam detection analysis.
/// </summary>
public sealed class DetectionResult
{
[JsonPropertyName("is_spam")]
public bool IsSpam { get; set; }
[JsonPropertyName("reason")]
public string Reason { get; set; } = string.Empty;
}
/// <summary>
/// Represents a generated email response.
/// </summary>
public sealed class EmailResponse
{
[JsonPropertyName("response")]
public string Response { get; set; } = string.Empty;
}
@@ -1,53 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable IDE0002 // Simplify Member Access
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI.Chat;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
// Two agents used by the orchestration to demonstrate conditional logic.
const string SpamDetectionName = "SpamDetectionAgent";
const string SpamDetectionInstructions = "You are a spam detection assistant that identifies spam emails.";
const string EmailAssistantName = "EmailAssistantAgent";
const string EmailAssistantInstructions = "You are an email assistant that helps users draft responses to emails with professionalism.";
AIAgent spamDetectionAgent = client.GetChatClient(deploymentName)
.AsAIAgent(SpamDetectionInstructions, SpamDetectionName);
AIAgent emailAssistantAgent = client.GetChatClient(deploymentName)
.AsAIAgent(EmailAssistantInstructions, EmailAssistantName);
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options =>
{
options
.AddAIAgent(spamDetectionAgent)
.AddAIAgent(emailAssistantAgent);
})
.Build();
app.Run();
@@ -1,113 +0,0 @@
# Multi-Agent Orchestration with Conditionals Sample
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a multi-agent orchestration workflow that includes conditional logic. The workflow implements a spam detection system that processes emails and takes different actions based on whether the email is identified as spam or legitimate.
## Key Concepts Demonstrated
- Multi-agent orchestration with conditional logic and different processing paths
- Spam detection using AI agent analysis
- Structured output from agents for reliable processing
- Activity functions for integrating non-agentic workflow actions
## Environment Setup
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
## Running the Sample
With the environment setup and function app running, you can test the sample by sending an HTTP request with email data to the orchestration.
You can use the `demo.http` file to send email data to the agents, or a command line tool like `curl` as shown below:
Bash (Linux/macOS/WSL):
```bash
# Test with a legitimate email
curl -X POST http://localhost:7071/api/spamdetection/run \
-H "Content-Type: application/json" \
-d '{
"email_id": "email-001",
"email_content": "Hi John, I hope you are doing well. I wanted to follow up on our meeting yesterday about the quarterly report. Could you please send me the updated figures by Friday? Thanks!"
}'
# Test with a spam email
curl -X POST http://localhost:7071/api/spamdetection/run \
-H "Content-Type: application/json" \
-d '{
"email_id": "email-002",
"email_content": "URGENT! You have won $1,000,000! Click here now to claim your prize! Limited time offer! Do not miss out!"
}'
```
PowerShell:
```powershell
# Test with a legitimate email
$body = @{
email_id = "email-001"
email_content = "Hi John, I hope you are doing well. I wanted to follow up on our meeting yesterday about the quarterly report. Could you please send me the updated figures by Friday? Thanks!"
} | ConvertTo-Json
Invoke-RestMethod -Method Post `
-Uri http://localhost:7071/api/spamdetection/run `
-ContentType application/json `
-Body $body
# Test with a spam email
$body = @{
email_id = "email-002"
email_content = "URGENT! You have won $1,000,000! Click here now to claim your prize! Limited time offer! Do not miss out!"
} | ConvertTo-Json
Invoke-RestMethod -Method Post `
-Uri http://localhost:7071/api/spamdetection/run `
-ContentType application/json `
-Body $body
```
The response from either input will be a JSON object that looks something like the following, which indicates that the orchestration has started.
```json
{
"message": "Spam detection orchestration started.",
"emailId": "email-001",
"instanceId": "555dbbb63f75406db2edf9f1f092de95",
"statusQueryGetUri": "http://localhost:7071/api/spamdetection/status/555dbbb63f75406db2edf9f1f092de95"
}
```
The orchestration will:
1. Analyze the email content using the SpamDetectionAgent
2. If spam: Mark the email as spam with a reason
3. If legitimate: Use the EmailAssistantAgent to draft a professional response and "send" it
Once the orchestration has completed, you can get the status of the orchestration by sending a GET request to the `statusQueryGetUri` URL. The response for the legitimate email will be a JSON object that looks something like the following:
```json
{
"failureDetails": null,
"input": {
"email_content": "Hi John, I hope you're doing well. I wanted to follow up on our meeting yesterday about the quarterly report. Could you please send me the updated figures by Friday? Thanks!",
"email_id": "email-001"
},
"instanceId": "555dbbb63f75406db2edf9f1f092de95",
"output": "Email sent: Subject: Re: Follow-Up on Quarterly Report\n\nHi [Recipient's Name],\n\nI hope this message finds you well. Thank you for your patience. I will ensure the updated figures for the quarterly report are sent to you by Friday.\n\nIf you have any further questions or need additional information, please feel free to reach out.\n\nBest regards,\n\nJohn",
"runtimeStatus": "Completed"
}
```
The response for the spam email will be a JSON object that looks something like the following, which indicates that the email was marked as spam:
```json
{
"failureDetails": null,
"input": {
"email_content": "URGENT! You have won $1,000,000! Click here now to claim your prize! Limited time offer! Do not miss out!",
"email_id": "email-002"
},
"instanceId": "555dbbb63f75406db2edf9f1f092de95",
"output": "Email marked as spam: The email contains misleading claims of winning a large sum of money and encourages immediate action, which are common characteristics of spam.",
"runtimeStatus": "Completed"
}
```
@@ -1,18 +0,0 @@
### Test spam detection with a legitimate email
POST http://localhost:7071/api/spamdetection/run
Content-Type: application/json
{
"email_id": "email-001",
"email_content": "Hi John, I hope you're doing well. I wanted to follow up on our meeting yesterday about the quarterly report. Could you please send me the updated figures by Friday? Thanks!"
}
### Test spam detection with a spam email
POST http://localhost:7071/api/spamdetection/run
Content-Type: application/json
{
"email_id": "email-002",
"email_content": "URGENT! You've won $1,000,000! Click here now to claim your prize! Limited time offer! Don't miss out!"
}
@@ -1,20 +0,0 @@
{
"version": "2.0",
"logging": {
"logLevel": {
"Microsoft.Agents.AI.DurableTask": "Information",
"Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
"DurableTask": "Information",
"Microsoft.DurableTask": "Information"
}
},
"extensions": {
"durableTask": {
"hubName": "default",
"storageProvider": {
"type": "AzureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
}
}
@@ -1,43 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- The Functions build tools don't like namespaces that start with a number -->
<AssemblyName>AgentOrchestration_HITL</AssemblyName>
<RootNamespace>AgentOrchestration_HITL</RootNamespace>
<NoWarn>$(NoWarn);DURABLE0001;DURABLE0002;DURABLE0003;DURABLE0004;DURABLE0005;DURABLE0006</NoWarn>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- Azure Functions packages -->
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask.AzureManaged" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<!-- Local projects that should be switched to package references when using the sample outside of this MAF repo -->
<!--
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Hosting.AzureFunctions" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" />
</ItemGroup>
-->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Hosting.AzureFunctions\Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -1,229 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;
namespace AgentOrchestration_HITL;
public static class FunctionTriggers
{
[Function(nameof(RunOrchestrationAsync))]
public static async Task<object> RunOrchestrationAsync(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
// Get the input from the orchestration
ContentGenerationInput input = context.GetInput<ContentGenerationInput>()
?? throw new InvalidOperationException("Content generation input is required");
// Get the writer agent
DurableAIAgent writerAgent = context.GetAgent("WriterAgent");
AgentSession writerSession = await writerAgent.CreateSessionAsync();
// Set initial status
context.SetCustomStatus($"Starting content generation for topic: {input.Topic}");
// Step 1: Generate initial content
AgentResponse<GeneratedContent> writerResponse = await writerAgent.RunAsync<GeneratedContent>(
message: $"Write a short article about '{input.Topic}'.",
session: writerSession);
GeneratedContent content = writerResponse.Result;
// Human-in-the-loop iteration - we set a maximum number of attempts to avoid infinite loops
int iterationCount = 0;
while (iterationCount++ < input.MaxReviewAttempts)
{
context.SetCustomStatus(
$"Requesting human feedback. Iteration #{iterationCount}. Timeout: {input.ApprovalTimeoutHours} hour(s).");
// Step 2: Notify user to review the content
await context.CallActivityAsync(nameof(NotifyUserForApproval), content);
// Step 3: Wait for human feedback with configurable timeout
HumanApprovalResponse humanResponse;
try
{
humanResponse = await context.WaitForExternalEvent<HumanApprovalResponse>(
eventName: "HumanApproval",
timeout: TimeSpan.FromHours(input.ApprovalTimeoutHours));
}
catch (OperationCanceledException)
{
// Timeout occurred - treat as rejection
context.SetCustomStatus(
$"Human approval timed out after {input.ApprovalTimeoutHours} hour(s). Treating as rejection.");
throw new TimeoutException($"Human approval timed out after {input.ApprovalTimeoutHours} hour(s).");
}
if (humanResponse.Approved)
{
context.SetCustomStatus("Content approved by human reviewer. Publishing content...");
// Step 4: Publish the approved content
await context.CallActivityAsync(nameof(PublishContent), content);
context.SetCustomStatus($"Content published successfully at {context.CurrentUtcDateTime:s}");
return new { content = content.Content };
}
context.SetCustomStatus("Content rejected by human reviewer. Incorporating feedback and regenerating...");
// Incorporate human feedback and regenerate
writerResponse = await writerAgent.RunAsync<GeneratedContent>(
message: $"""
The content was rejected by a human reviewer. Please rewrite the article incorporating their feedback.
Human Feedback: {humanResponse.Feedback}
""",
session: writerSession);
content = writerResponse.Result;
}
// If we reach here, it means we exhausted the maximum number of iterations
throw new InvalidOperationException(
$"Content could not be approved after {input.MaxReviewAttempts} iterations.");
}
// POST /hitl/run
[Function(nameof(StartOrchestrationAsync))]
public static async Task<HttpResponseData> StartOrchestrationAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "hitl/run")] HttpRequestData req,
[DurableClient] DurableTaskClient client)
{
// Read the input from the request body
ContentGenerationInput? input = await req.ReadFromJsonAsync<ContentGenerationInput>();
if (input is null || string.IsNullOrWhiteSpace(input.Topic))
{
HttpResponseData badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await badRequestResponse.WriteAsJsonAsync(new { error = "Topic is required" });
return badRequestResponse;
}
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
orchestratorName: nameof(RunOrchestrationAsync),
input: input);
HttpResponseData response = req.CreateResponse(HttpStatusCode.Accepted);
await response.WriteAsJsonAsync(new
{
message = "HITL content generation orchestration started.",
topic = input.Topic,
instanceId,
statusQueryGetUri = GetStatusQueryGetUri(req, instanceId),
});
return response;
}
// POST /hitl/approve/{instanceId}
[Function(nameof(SendHumanApprovalAsync))]
public static async Task<HttpResponseData> SendHumanApprovalAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "hitl/approve/{instanceId}")] HttpRequestData req,
string instanceId,
[DurableClient] DurableTaskClient client)
{
// Read the approval response from the request body
HumanApprovalResponse? approvalResponse = await req.ReadFromJsonAsync<HumanApprovalResponse>();
if (approvalResponse is null)
{
HttpResponseData badRequestResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await badRequestResponse.WriteAsJsonAsync(new { error = "Approval response is required" });
return badRequestResponse;
}
// Send the approval event to the orchestration
await client.RaiseEventAsync(instanceId, "HumanApproval", approvalResponse);
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new
{
message = "Human approval sent to orchestration.",
instanceId,
approved = approvalResponse.Approved
});
return response;
}
// GET /hitl/status/{instanceId}
[Function(nameof(GetOrchestrationStatusAsync))]
public static async Task<HttpResponseData> GetOrchestrationStatusAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "hitl/status/{instanceId}")] HttpRequestData req,
string instanceId,
[DurableClient] DurableTaskClient client)
{
OrchestrationMetadata? status = await client.GetInstanceAsync(
instanceId,
getInputsAndOutputs: true,
req.FunctionContext.CancellationToken);
if (status is null)
{
HttpResponseData notFound = req.CreateResponse(HttpStatusCode.NotFound);
await notFound.WriteAsJsonAsync(new { error = "Instance not found" });
return notFound;
}
HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new
{
instanceId = status.InstanceId,
runtimeStatus = status.RuntimeStatus.ToString(),
workflowStatus = status.SerializedCustomStatus is not null ? (object)status.ReadCustomStatusAs<JsonElement>() : null,
input = status.SerializedInput is not null ? (object)status.ReadInputAs<JsonElement>() : null,
output = status.SerializedOutput is not null ? (object)status.ReadOutputAs<JsonElement>() : null,
failureDetails = status.FailureDetails
});
return response;
}
[Function(nameof(NotifyUserForApproval))]
public static void NotifyUserForApproval(
[ActivityTrigger] GeneratedContent content,
FunctionContext functionContext)
{
ILogger logger = functionContext.GetLogger(nameof(NotifyUserForApproval));
// In a real implementation, this would send notifications via email, SMS, etc.
logger.LogInformation(
"""
NOTIFICATION: Please review the following content for approval:
Title: {Title}
Content: {Content}
Use the approval endpoint to approve or reject this content.
""",
content.Title,
content.Content);
}
[Function(nameof(PublishContent))]
public static void PublishContent(
[ActivityTrigger] GeneratedContent content,
FunctionContext functionContext)
{
ILogger logger = functionContext.GetLogger(nameof(PublishContent));
// In a real implementation, this would publish to a CMS, website, etc.
logger.LogInformation(
"""
PUBLISHING: Content has been published successfully.
Title: {Title}
Content: {Content}
""",
content.Title,
content.Content);
}
private static string GetStatusQueryGetUri(HttpRequestData req, string instanceId)
{
// NOTE: This can be made more robust by considering the value of
// request headers like "X-Forwarded-Host" and "X-Forwarded-Proto".
string authority = $"{req.Url.Scheme}://{req.Url.Authority}";
return $"{authority}/api/hitl/status/{instanceId}";
}
}
@@ -1,44 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json.Serialization;
namespace AgentOrchestration_HITL;
/// <summary>
/// Represents the input for the Human-in-the-Loop content generation workflow.
/// </summary>
public sealed class ContentGenerationInput
{
[JsonPropertyName("topic")]
public string Topic { get; set; } = string.Empty;
[JsonPropertyName("max_review_attempts")]
public int MaxReviewAttempts { get; set; } = 3;
[JsonPropertyName("approval_timeout_hours")]
public float ApprovalTimeoutHours { get; set; } = 72;
}
/// <summary>
/// Represents the content generated by the writer agent.
/// </summary>
public sealed class GeneratedContent
{
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("content")]
public string Content { get; set; } = string.Empty;
}
/// <summary>
/// Represents the human approval response.
/// </summary>
public sealed class HumanApprovalResponse
{
[JsonPropertyName("approved")]
public bool Approved { get; set; }
[JsonPropertyName("feedback")]
public string Feedback { get; set; } = string.Empty;
}
@@ -1,45 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable IDE0002 // Simplify Member Access
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AzureFunctions;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI.Chat;
// Get the Azure OpenAI endpoint and deployment name from environment variables.
string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
: new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential());
// Single agent used by the orchestration to demonstrate human-in-the-loop workflow.
const string WriterName = "WriterAgent";
const string WriterInstructions =
"""
You are a professional content writer who creates high-quality articles on various topics.
You write engaging, informative, and well-structured content that follows best practices for readability and accuracy.
""";
AIAgent writerAgent = client.GetChatClient(deploymentName).AsAIAgent(WriterInstructions, WriterName);
using IHost app = FunctionsApplication
.CreateBuilder(args)
.ConfigureFunctionsWebApplication()
.ConfigureDurableAgents(options => options.AddAIAgent(writerAgent))
.Build();
app.Run();
@@ -1,126 +0,0 @@
# Multi-Agent Orchestration with Human-in-the-Loop Sample
This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a human-in-the-loop (HITL) workflow using a single AI agent. The workflow uses a writer agent to generate content and requires human approval on every iteration, emphasizing the human-in-the-loop pattern.
## Key Concepts Demonstrated
- Single-agent orchestration
- Human-in-the-loop feedback loop using external events (`WaitForExternalEvent`)
- Activity functions for non-agentic workflow steps
- Iterative content refinement based on human feedback
- Custom status tracking for workflow visibility
- Error handling with maximum retry attempts and timeout handling for human approval
## Environment Setup
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
## Running the Sample
With the environment setup and function app running, you can test the sample by sending an HTTP request with a topic to start the content generation workflow.
You can use the `demo.http` file to send a topic to the agents, or a command line tool like `curl` as shown below:
Bash (Linux/macOS/WSL):
```bash
curl -X POST http://localhost:7071/api/hitl/run \
-H "Content-Type: application/json" \
-d '{
"topic": "The Future of Artificial Intelligence",
"max_review_attempts": 3,
"timeout_minutes": 5
}'
```
PowerShell:
```powershell
$body = @{
topic = "The Future of Artificial Intelligence"
max_review_attempts = 3
timeout_minutes = 5
} | ConvertTo-Json
Invoke-RestMethod -Method Post `
-Uri http://localhost:7071/api/hitl/run `
-ContentType application/json `
-Body $body
```
The response will be a JSON object that looks something like the following, which indicates that the orchestration has started.
```json
{
"message": "HITL content generation orchestration started.",
"topic": "The Future of Artificial Intelligence",
"instanceId": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"statusQueryGetUri": "http://localhost:7071/api/hitl/status/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
}
```
The orchestration will:
1. Generate initial content using the WriterAgent
2. Notify the user to review the content
3. Wait for human feedback via external event (configurable timeout)
4. If approved by human, publish the content
5. If rejected by human, incorporate feedback and regenerate content
6. If approval timeout occurs, treat as rejection and fail the orchestration
7. Repeat until human approval is received or maximum loop iterations are reached
Once the orchestration is waiting for human approval, you can send approval or rejection using the approval endpoint:
Bash (Linux/macOS/WSL):
```bash
# Approve the content
curl -X POST http://localhost:7071/api/hitl/approve/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 \
-H "Content-Type: application/json" \
-d '{
"approved": true,
"feedback": "Great article! The content is well-structured and informative."
}'
# Reject the content with feedback
curl -X POST http://localhost:7071/api/hitl/approve/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 \
-H "Content-Type: application/json" \
-d '{
"approved": false,
"feedback": "The article needs more technical depth and better examples."
}'
```
PowerShell:
```powershell
# Approve the content
Invoke-RestMethod -Method Post `
-Uri http://localhost:7071/api/hitl/approve/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 `
-ContentType application/json `
-Body '{ "approved": true, "feedback": "Great article! The content is well-structured and informative." }'
# Reject the content with feedback
Invoke-RestMethod -Method Post `
-Uri http://localhost:7071/api/hitl/approve/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 `
-ContentType application/json `
-Body '{ "approved": false, "feedback": "The article needs more technical depth and better examples." }'
```
Once the orchestration has completed, you can get the status by sending a GET request to the `statusQueryGetUri` URL. The response will be a JSON object that looks something like the following:
```json
{
"failureDetails": null,
"input": {
"topic": "The Future of Artificial Intelligence",
"max_review_attempts": 3
},
"instanceId": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"output": {
"content": "The Future of Artificial Intelligence is..."
},
"runtimeStatus": "Completed",
"workflowStatus": "Content published successfully at 2025-10-15T12:00:00Z"
}
```

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