Compare commits

..

66 Commits

Author SHA1 Message Date
Giles Odigwe 7a2b8038cc [BREAKING] Python: Bump package versions for 1.15.0 release (#7812)
* Bump Python package versions for 1.15.0 release

Prepare the CHANGELOG-selected Python packages for the 1.15.0 release. Root and core move to 1.15.0; changed stable extensions receive package-specific minor or patch bumps; changed beta packages receive the 260821 stamp; no beta cohort bump is applied. Core dependency floors use the conservative policy for co-released packages. Release validation also adds the six dependency required by the supported Azure Cosmos SDK floor and retains cross-platform-compatible development-tool pins.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 98e979bd-1d07-41fd-946d-00db8a93e248

* Remove hook-only formatting changes

Keep the Python 1.15.0 release commit scoped to package metadata, release notes, dependency floors, and the lockfile.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 98e979bd-1d07-41fd-946d-00db8a93e248

* Minimize release lockfile changes

Restore the upstream PyPI-backed lockfile and retain only package versions and dependency metadata changed by the Python 1.15.0 release. Also preserve the development-tool upgrades already present on main.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 98e979bd-1d07-41fd-946d-00db8a93e248

* Retain OpenAI core compatibility floor

Keep agent-framework-openai 1.13.1 compatible with core 1.13 because its streaming tool-call index fix uses the existing additional_properties API and does not require core 1.15.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 98e979bd-1d07-41fd-946d-00db8a93e248

* Raise OpenAI version and core floor

Bump agent-framework-openai to 1.14.0 and require core 1.15.0 so the new dependency requirement is signaled as a minor release.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 98e979bd-1d07-41fd-946d-00db8a93e248

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 98e979bd-1d07-41fd-946d-00db8a93e248
2026-08-21 23:03:18 +00:00
Evan Mattson 65e8aff93b Fix DevFlow review command whitespace handling (#7813) 2026-08-21 22:37:32 +00:00
Giles Odigwe c6a0e90250 Python: correct MCP tool argument filtering documentation (#7801)
* Python: correct MCP tool argument filtering documentation

The documentation for MCPTool's outbound argument filtering did not match
its behavior. The comment on _prepare_call_kwargs stated that framework
runtime kwargs are "stripped so it is never forwarded to the MCP server",
and packages/core/AGENTS.md repeated the same claim.

In practice, runtime kwargs (FunctionInvocationContext.kwargs, seeded from
function_invocation_kwargs) are merged with the model-supplied arguments in
_call_tool_with_runtime_kwargs before the filter runs, so provenance is no
longer distinguishable at that point. The allowlist is built from the tool's
declared inputSchema.properties as advertised by the server, plus names opted
in through additional_tool_argument_names. A runtime kwarg is therefore
forwarded whenever the server declares a property of the same name, without
the model supplying it.

Update the comments, docstrings and docs to describe the actual rule, and
point each transport at its appropriate channel for values that should not
become tool arguments (env for stdio, header_provider for streamable HTTP).

Also narrow the docstring of test_call_tool_forwards_only_declared_arguments,
which claimed more than it asserts (it covers undeclared names only), and add
a companion test pinning the declared-name behavior so the documented rule
stays verifiable.

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

* Python: address review feedback on MCP argument filtering docs

Corrects and tightens the documentation added in the previous commit.

- header_provider does not withhold values from the outbound argument
  filter; it reads the runtime kwargs without consuming them. The earlier
  wording recommended it as a way to keep a value out of tool arguments,
  which is wrong. Replaced in four places with the pattern that does work:
  source the credential outside function_invocation_kwargs, for example by
  reading a ContextVar inside the provider, which still allows a different
  value per request.
- Note the _meta key and the framework denylist as exceptions wherever the
  docs say server-declared names are forwarded.
- Rework test_call_tool_forwards_runtime_kwargs_the_server_declares to
  invoke the generated FunctionTool with a FunctionInvocationContext, so it
  exercises the real runtime-kwargs path instead of calling call_tool
  directly. Verified by mutation: removing the merge in
  _call_tool_with_runtime_kwargs now fails the test.
- Add a test covering the recommended ContextVar pattern.
- Condense the transport docstring notes, which had grown into three
  near-duplicate blocks.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-21 18:00:58 +00:00
Tao Chen 7b7b9a128c Python: Foundry Hosted Agent Resiliency Support (#7670)
* 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

* LRA on top of state store

* Temp disable state store user isolation

* Simulate shutdown

* Remove sim shutdown

* Add sample

* refine resiliency sample

* Add steerable conversation support

* Revert uv.lock

* Add last_checkpoint_id and checkpoint existence check

* Tighted resilient-recovery states

* Tests for tightened resilient-recovery states

* Make cancellation effective even when the iterator is stuck

* Add more tests and fix sample

* Small adjustment after review

* Fix typing

* Fix typing

* Close driver background task in case of exceptions raised in the consumer

* Handle usage content

* xfail an integration test due to a known gap

* Fix formatting

---------

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-08-21 17:37:47 +00:00
Quim Muntal eb8548e582 Docs: add Go to the main README (#7754)
* docs: add Go to main README

* docs: address Go README review feedback

* docs: refine Go support wording

* docs: preserve focused contributor resources

* docs: scope Go reference to separate repository

* Apply batched suggestions from code review

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

---------

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2026-08-21 16:29:04 +00:00
dependabot[bot] ede605d868 .NET: Bump AgentMemory from 1.3.0 to 1.4.1 (#7639)
* Bump AgentMemory from 1.3.0 to 1.4.1

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

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

* Potential fix for pull request finding

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-21 12:51:26 +00:00
Chandramouleswaran eccf47154f .NET: Fix ReasoningSummary passthrough in GitHub Copilot resume config (#6441)
* .NET: Fix ReasoningSummary passthrough in GitHub Copilot resume config

CopyResumeSessionConfig hand-copies a subset of SessionConfigBase into a new ResumeSessionConfig instead of using Clone(). It was missing ReasoningSummary, so callers that set SessionConfig.ReasoningSummary got readable extended-thinking summaries on the first turn but had it silently dropped on every resumed turn. ContextTier (a sibling model/context knob passed alongside ReasoningEffort/ReasoningSummary) was missing too. Both are now copied, mirroring ReasoningEffort.

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

* .NET: Assert ReasoningSummary/ContextTier defaults in null-source resume test

Addresses PR review: the null-source CopyResumeSessionConfig test now also asserts ReasoningSummary and ContextTier default to null, locking the intended default behavior of the newly copied properties.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2026-08-21 10:25:26 +00:00
dependabot[bot] 6a58888f3a Python: Bump uv from 0.11.32 to 0.12.5 in /python (#7780)
* Bump uv from 0.11.32 to 0.12.5 in /python

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

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

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

* Align lab uv pin to 0.12.5 and update uv.lock

Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com>
2026-08-21 09:41:23 +00:00
dependabot[bot] 8f68f6be5d Bump Anthropic from 12.35.1 to 12.42.0 (#7778)
* Bump Anthropic from 12.35.1 to 12.42.0

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

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

* Update Anthropic test clients for SDK interfaces

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
2026-08-21 09:30:29 +00:00
Ran Shemtov 59ecceba03 Python: A2UI (Agent-to-UI) support for the AG-UI adapter (#7423)
* Python: A2UI (Agent-to-UI) support for the AG-UI adapter

Adds an in-package _a2ui module to agent-framework-ag-ui delivering
progressive-streaming, error-recovery, and sub-agent-based A2UI surface
generation, reusing the shared ag-ui-a2ui-toolkit. Includes example
agents, a unit suite, and two bridge fixes (strip unanswered tool calls
from replayed history; suppress the terminal MESSAGES_SNAPSHOT for A2UI
runs to keep streamed order stable).

Signed-off-by: ran <ran@copilotkit.ai>

* Python: A2UI review feedback — declarative wiring, no agent swap

Reworks A2UI so it no longer swaps the agent object mid-run, and fixes the
issues that swap caused.

- Drive A2UI through a dedicated runner used only for the stream call; keep
  the original agent bound so protected-state-key computation, approval
  resolution, and continuation serialization still read its real
  context_providers and client (no more provider-namespace or approval
  middleware loss).
- Hand the forwarded AG-UI context to the runner directly instead of stamping
  it onto run-option additional_properties. That channel leaked the slice to
  the provider SDK on any run carrying AG-UI context, including non-A2UI runs
  where nothing stripped it back. Removes the stamp/strip/read helpers and the
  dead .NET-shaped path.
- Suppress the terminal MESSAGES_SNAPSHOT off whether A2UI actually drove the
  run, not the literal tool names, so an unrelated user tool named
  "generate_a2ui" keeps its snapshot.
- Fail loud with an install hint when A2UI is requested but the toolkit isn't
  installed, instead of advertising render_a2ui with no executor.
- Include the agent's own default tools in the no-double-injection check so an
  already-wired agent doesn't crash on a duplicate tool name.
- Execute ordinary developer tools called in the same turn as generate_a2ui
  (the declaration-only tool poisons the inner batch invocation), so a
  "look up data then render it" turn no longer skips the backend call.
- Attribute nameless streaming argument deltas by the provider tool-call index
  so interleaved parallel calls don't cross-contaminate; the OpenAI chat client
  preserves that index on the content.

Adds tests for the mixed-batch execution, index-based fragment attribution,
and the default-tool duplicate check.

Signed-off-by: ran <ran@copilotkit.ai>

* Python: A2UI review round 2 — mixed-batch pipeline, client tools, snapshot

- Mixed-batch (a tool called in the same turn as generate_a2ui): execute
  server tools through the agent's real function-invocation pipeline (client
  function_middleware + config), the same path approval-resume uses, instead
  of a direct tool.invoke() that bypassed middleware/context/session.
- Look up mixed-batch tools across incoming AND the agent's own default tools,
  so a server tool wired only on the agent (no runtime tools=) still executes.
- Leave declaration-only client tools (func=None) as user-input requests
  instead of synthesizing a local result, preserving the resumable client-tool
  flow.
- Recognize a manually enable_a2ui()-wrapped agent when deciding to suppress
  the terminal MESSAGES_SNAPSHOT, so the ordering fix also covers that path.
- Remove .NET-specific comments from the Python module.

Adds tests: server-tool execution runs through middleware, default-tool
execution, client declaration-only tool left as user-input.

Signed-off-by: ran <ran@copilotkit.ai>

* Python: A2UI review round 2 — fold context wrapper, typed runner

Consolidates A2UI wiring into one owner, per review:

- Fold the context-prepend (former AGUIContextAgent) into A2UIAgent, which now
  prepends the forwarded catalog + guidelines as a system message itself.
  Removes the extra agent type (matching the langgraph/strands adapters, which
  have no separate context agent).
- Make A2UIAgent the typed runner interface: it carries the render tool(s) to
  strip (drop_tool_names) and is recognized via is_a2ui_runner(). plan_a2ui_injection
  now returns the runner (or None) instead of a bare dict, so no private plan keys
  leak into the host and the host no longer tracks activation separately —
  is_a2ui_runner() covers both the auto-injected and manual enable_a2ui() paths.

Signed-off-by: ran <ran@copilotkit.ai>

* Python: A2UI — bridge test for client tool + generate_a2ui in one turn

End-to-end through run_agent_stream: a turn that calls a declaration-only client
tool alongside generate_a2ui surfaces the client tool as a resumable frontend
tool call (START/ARGS/END, no server-synthesized result) so the frontend
executes and resumes it, the A2UI surface still renders, the run finishes, and
no terminal MESSAGES_SNAPSHOT is emitted (manual enable_a2ui path). Confirms the
mixed-batch client-tool contract on the AG-UI wire, not just at the agent level.

Signed-off-by: ran <ran@copilotkit.ai>

* Python: A2UI review round 3 — manual-path delegation, per-request context, facade

- A2UIAgent delegates client / default_options / context_providers to the wrapped
  agent, so a manually enable_a2ui()-wrapped runner keeps the inner agent's
  configured tools, provider-owned state protection, and approval middleware that
  the auto-injected path already preserves.
- Take the AG-UI context slice per run (a2ui_context kwarg the host passes each
  request) instead of only at construction, so a reused runner never serves stale
  catalog/guidelines.
- Remove the deleted AGUIContextAgent from the package facade's __all__ and lazy
  exports (it no longer resolves) and drop the remaining doc references to it.

Signed-off-by: ran <ran@copilotkit.ai>

* Python: A2UI review round 3 — mixed-batch reuses the core invocation controls

Reworks server-tool execution batched with generate_a2ui so it goes through the
shared function-invocation owner faithfully instead of a partial re-implementation:

- Pass the run's invocation session and the full function-middleware pipeline
  (static client middleware plus runtime middleware) into the execution.
- Honor function_invocation_configuration["enabled"] and the shared per-request
  max_function_calls budget, tracked cumulatively across A2UI planner rounds, so a
  side-effecting tool cannot run once per round or run while invocation is disabled.
- Preserve non-result control contents (e.g. a function_approval_request for an
  always_require tool) and the executor's termination signal instead of filtering to
  function_result, and surface them on the wire.
- Stop the run instead of re-entering the planner whenever the turn carries calls it
  cannot safely replay — client tools awaiting the frontend, deferred/over-budget or
  approval-pending server tools, or a termination request — so an unanswered assistant
  tool_call is never replayed as unbalanced history.

Tests: cumulative budget cap across rounds, invocation-disabled skip, approval request
surfaced + run stops, and the bridge test now asserts the planner is not re-entered.

Signed-off-by: ran <ran@copilotkit.ai>

* Python: A2UI review round 4 — core batch executor, budget/iteration parity

- Add a core-owned execute_function_call_batch() to agent_framework._tools that
  builds the function-middleware pipeline (static + runtime, normalizing bare
  objects and expanding MiddlewareBundles via categorize_middleware), normalizes
  config, threads the invocation session, and returns a structured result
  (results / control / should_terminate). A2UI's mixed-batch server execution now
  delegates to it instead of reproducing the pipeline/session/result handling, so
  a runtime `middleware=<bare>` or a bundle no longer raises or is silently skipped,
  and future core policy changes stay in one place.
- Charge generate_a2ui against the per-request max_function_calls budget (each is a
  render-subagent invocation) and cap the planner rounds by max_iterations, so a
  generate-only planner can no longer run more render calls than the configured
  limits.

Tests: generate-only planner honors the call budget and max_iterations; the
mixed-batch budget test accounts for generate also charging.

Signed-off-by: ran <ran@copilotkit.ai>

* Python: A2UI review round 5 — force tools off on the final narration turn

The final narration turn started a fresh inner_agent.run() with tools still
enabled, so after the planner rounds/budget were spent it could execute another
full batch of server/default tools and exceed max_function_calls / max_iterations.
Set tool_choice="none" on that turn so it is a pure narration with no tool
execution, matching the core loop's budget-exhausted final response.

Test: the final narration turn's options carry tool_choice="none".
Signed-off-by: ran <ran@copilotkit.ai>

* Python: A2UI review round 5 — move the budget lifecycle into a core owner

Add a core-owned FunctionCallBudget to agent_framework._tools that owns the
per-request accounting the core loop enforces: the invocation toggle, the
cumulative max_function_calls budget, the max_iterations round cap, and the
tools-off final-response options. execute_function_call_batch now takes a budget
and returns the deferred (unrun) calls.

A2UIAgent's planner loop no longer reimplements any of this — it holds one budget
object and asks it (rounds_remaining / take / exhausted / final_response_options),
so server tools, generate_a2ui, the round cap, and the final tools-off turn all go
through the single core owner. This removes the split that let the final turn start
a fresh budget, and keeps mixed A2UI turns aligned with core policy changes.

Tests: core budget primitive (take/exhausted/rounds/final-options); invocation
disabled now runs no server tool AND no surface (matches the core loop).

Signed-off-by: ran <ran@copilotkit.ai>

* Python: A2UI review round 5 follow-up — keep budgeting local, narrate on budget exhaustion

Per review, the core is not the place for a second budget abstraction: remove the
FunctionCallBudget class from agent_framework._tools (execute_function_call_batch,
which was the requested shared executor, stays). A2UIAgent honors the inner agent's
function-invocation configuration locally again — the invocation toggle, the
cumulative max_function_calls budget (charged by server tools and generate_a2ui),
and the max_iterations round cap.

Also fix the reported gap: when the call budget is exhausted (e.g. max_function_calls=1
spent on the first generate_a2ui), the run now breaks to the tools-off final narration
turn instead of returning after the surface, so it produces a closing assistant
response — matching the iteration-cap path and the core loop. Calls awaiting external
resolution (client tools, deferred, approval, termination) still end the run without
that final turn, since a follow-up run resumes them.

Signed-off-by: ran <ran@copilotkit.ai>

* Python: A2UI — feed the current surface to the budget-exhausted final narration

On budget exhaustion the loop broke before appending this round's assistant
tool_call(s) and results to history, so the tools-off final narration turn saw
only the original user messages and could not narrate the generate_a2ui result it
had just produced. Append the round's assistant/tool pair before breaking so the
final turn receives it. The test now asserts the final turn's messages include the
just-produced surface.

Signed-off-by: ran <ran@copilotkit.ai>

* Python: A2UI — keep batch execution in the adapter; fix CI typing

Per review, don't add A2UI-specific abstractions to core: remove
execute_function_call_batch / FunctionCallBatchExecution from
agent_framework._tools. A2UIAgent's _execute_server_tools now runs the mixed
batch inline using the framework's existing helpers (_try_execute_function_call_groups
plus categorize_middleware for bare/bundle middleware normalization) with the run's
session, config, and middleware — the same helpers the AG-UI approval path uses — so
nothing adapter-specific lives in core.

Also fix the CI typing check: annotate the A2UI test doubles and helpers so mypy,
pyrefly, and ty pass over the test module (mixed-shape result tuples, a nullable
envelope helper, and duck-typed fakes passed where protocols are expected).

Signed-off-by: ran <ran@copilotkit.ai>

* Python: A2UI — propagate MiddlewareFailure through the inline server-tool path; generic non-leaking error results; fix ty test typing

- _execute_server_tools now re-raises MiddlewareFailure so a fail-closed
  authorization/guardrail abort stops the run instead of being folded into an
  error result that would still render a surface (matches the core loop).
- Ordinary execution failures return core's generic 'Error: Function failed.'
  message; the raw exception text rides the non-model-visible exception field
  and is only exposed when include_detailed_errors is enabled, so credentials/
  provider payloads/tenant data cannot leak to the model.
- Add ty suppressions on the two duck-typed test constructors (ty does not honor
  mypy-style '# type: ignore[arg-type]') to clear the Test Typing Checks gate.
- Cover both behaviors with tests (MiddlewareFailure aborts without rendering;
  tool error result is generic and non-leaking).

---------

Signed-off-by: ran <ran@copilotkit.ai>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-08-21 09:29:34 +00:00
dependabot[bot] a7fea02070 Python: Bump ruff from 0.16.0 to 0.16.3 in /python (#7781)
* Bump ruff from 0.16.0 to 0.16.3 in /python

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

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

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

* Align lab ruff pin to 0.16.3 and refresh uv.lock

Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com>

* Revert unintended ruff rule-name rewrites in python/pyproject.toml

Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com>
2026-08-21 08:57:17 +00:00
dependabot[bot] a8b0704691 Python: Bump ty from 0.0.70 to 0.0.72 in /python (#7783)
* Bump ty from 0.0.70 to 0.0.72 in /python

Bumps [ty](https://github.com/astral-sh/ty) from 0.0.70 to 0.0.72.
- [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.70...0.0.72)

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

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

* Regenerate uv.lock for ty 0.0.72 bump

Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com>
2026-08-21 08:56:56 +00:00
dependabot[bot] d1dbce7138 Python: Bump mypy from 2.3.0 to 2.3.1 in /python (#7784)
* Bump mypy from 2.3.0 to 2.3.1 in /python

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

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 2.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* Regenerate uv.lock for mypy 2.3.1 and align lab dev pin

Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com>
2026-08-21 08:56:01 +00:00
Sadok Barbouche 007a2d7a05 Python: FoundryEvals always emits arguments field for tool calls (#7734)
* Python: always emit arguments field for tool calls in AgentEvalConverter

FoundryEvals uploaded tool_call content items without an arguments
field when a tool call had no model-supplied arguments. Foundry's
tool-aware evaluators (task_adherence, tool_output_utilization,
tool_call_accuracy) require the arguments field to always be present,
so zero-argument tool calls caused evaluation to fail with
FAILED_EXECUTION. Default to an empty object instead of omitting the
field.

* Python: only default arguments to {} when None, not on falsy values

Addresses Copilot review feedback: a truthiness check would also
overwrite valid but falsy parsed arguments (e.g. 0, "", False) with
{}. Use an explicit None check so only missing arguments are defaulted.
2026-08-21 06:51:42 +00:00
Manjunath Janardhan 24a383613b Python: feat: forward function_invocation_kwargs through DevUI to agent.run (#7779)
DevUI's /v1/responses endpoint builds agent.run() kwargs by hand and only
passed stream/session, so tools that read request-scoped values via
FunctionInvocationContext.kwargs (tenant id, auth token, user id)
silently received nothing when the agent was run through DevUI. The same
agent works correctly outside DevUI via agent.run(..., function_invocation_kwargs=...).

Forward function_invocation_kwargs from the request into agent.run() in
AgentFrameworkExecutor._execute_agent. Accepts both channels already used
on the request payload:
  - extra_body.function_invocation_kwargs (the channel already used for
    response_id / checkpoint_id)
  - top-level extra field (AgentFrameworkRequest has ConfigDict(extra="allow"))
Top-level takes precedence when both are set. Non-dict / missing values
are silently ignored for backward compatibility. No frontend / model
changes.

Adds a parametrized regression test in test_execution.py covering all
three cases (extra_body, top-level, absent).

Fixes #7344
2026-08-21 06:28:36 +00:00
dependabot[bot] 946ece61a1 Update flit-core requirement from <4.0,>=3.11 to >=3.11,<5.0 in /python (#7782)
Updates the requirements on [flit-core](https://github.com/pypa/flit) to permit the latest version.
- [Changelog](https://github.com/pypa/flit/blob/main/doc/history.rst)
- [Commits](https://github.com/pypa/flit/compare/3.11.0...4.0.2)

---
updated-dependencies:
- dependency-name: flit-core
  dependency-version: 4.0.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-21 05:54:04 +00:00
Evan Mattson 074d23d269 Add moonbox3 as codeowner for Foundry hosting and local packages (#7804) 2026-08-21 04:17:49 +00:00
Tao Chen 4e754a636d [BREAKING] Python: Consolidate OTel GenAI Semantic Conventions versions (#7673)
* Consolidate OTel GenAI Semantic Conventions versions

* Address comments

* Refinement

* Further constraint v1.26.0 attrs

* Fix tests and typing

* Address copilot comments

* Fix tests

* Fix typing

* Fix typing

* Rewording
2026-08-21 01:41:06 +00:00
Peter Ibekwe 43b3ce6027 .NET: Add feature-usage bitmask (#7709)
* Add feature-usage User-Agent telemetr

* Removed static keyword from irrelevant methods.

* Update method names and fix CI test issue.

* Revert irrelevant changes.

* Address PR comments.

* Fix CI issue from merge conflict resolution.
2026-08-21 01:38:05 +00:00
Giles Odigwe 1109cf778b Python: resolve release tags against real package directories (#7795)
* Make python-release tag handling more robust

Pass the release tag through the step env block and reference it as a
quoted shell variable, and validate the package name derived from the
tag before using it as a directory path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 73f2e039-3ff4-4a5a-a8f8-253b0bfa94f3

* Resolve release tags against real package directories

Package names can contain hyphens and can use underscores where the tag
uses hyphens, so splitting the tag on the first hyphen picked the wrong
directory. Resolve the name against the actual packages/ listing instead,
and handle the python-<version> workspace tag explicitly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 73f2e039-3ff4-4a5a-a8f8-253b0bfa94f3

* Require a real version component in release tags

Selecting the workspace build on the absence of a hyphen meant a
malformed tag such as python-devui built and uploaded the whole
workspace. Match the suffix against the supported version formats
instead, and reject tags that are neither shape.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 73f2e039-3ff4-4a5a-a8f8-253b0bfa94f3

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 73f2e039-3ff4-4a5a-a8f8-253b0bfa94f3
2026-08-21 01:29:30 +00:00
Giles Odigwe aeaabe5abf Python: fix MCP tool argument shadowing the remote tool name (#7799)
* Python: fix MCP tool argument shadowing the remote tool name

The generated MCP function held the remote tool name as the default of a
keyword-only parameter. Tool arguments are splatted into that function, so an
argument named `_remote_tool_name` bound to the parameter instead of `**kwargs`
and changed which remote tool was called.

Move the remote tool name into a factory closure so it is no longer part of the
generated function's signature, matching the prompt path which already binds the
name positionally via `partial`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4cb58e38-4af2-485d-b734-7d70972959f2

* Guard await_args before indexing in MCP regression test

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

Copilot-Session: 4cb58e38-4af2-485d-b734-7d70972959f2

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4cb58e38-4af2-485d-b734-7d70972959f2
2026-08-21 01:26:37 +00:00
Peter Ibekwe 96560bbf65 .NET: [BREAKING] Migrate MCP long-running task support to the 2026-07-28 Tasks extension (#7774)
* Migrate MCP long-running task support to the 2026-07-28 Tasks extension

* Address PR comments.

* Address PR comments.

* Address PR comments
2026-08-20 23:26:28 +00:00
Roger Barreto ab0f7d5d08 .NET: Persist hosted agent state in Foundry (#7649)
* Point the AgentServer packages at the local preview drop

The durable state-store API this branch is built on ships in Core
beta.28, which is not on nuget.org yet. The local feed is a stopgap for
developing against it and must be removed before this branch ships.

* Keep hosted agent state on the platform instead of the container disk

A hosted agent kept its sessions, and a hosted workflow its checkpoints,
in files under the container's own directory. That state is lost when
the container is replaced and cannot be read by another instance of the
same agent, so a conversation could not survive a restart or be served
by more than one instance.

Both now go to the Foundry durable state store when the process runs in
a Foundry container, and stay on disk everywhere else:

- FoundryAgentSessionStore holds the agent sessions, partitioned by
  agent, conversation and end user.
- FoundryJsonCheckpointStore holds the workflow checkpoints, one item
  per checkpoint plus a per-session index that keeps them in commit
  order. Retrieving a checkpoint deletes the rest of that session's
  checkpoints, which is the only point at which nothing can still reach
  them, and is what stops the index growing past the size the platform
  accepts for one item.

A workflow agent is redirected to that checkpoint store when it is
resolved for a request, so nothing changes in how a container registers
one. An agent built with a checkpoint manager of its own is left alone
and reported by the new foundry-workflow-checkpointing readiness check,
because its state would go somewhere hosting does not manage.

Workflow agents are recognised through a new WorkflowAgentMetadata
returned by GetService, which still finds them behind middleware.

* Keep the readiness probe from running the agent's providers

The stored-output probe ran the registered agent with its chat client
replaced, which still set the agent's chat history provider and context
providers running. Those are the parts most likely to reach outside the
container and to write state, so every readiness probe could make
external calls and add its own empty turn to real conversations.

The probe now runs a stand-in built from the agent's own options with
both kinds of provider dropped. It keeps what decides the setting, the
chat options and the raw request factory, and cannot see a decorator
wrapped around the agent, which is accepted for a readiness check.

* build: bump AgentServer preview packages

Core beta.29 adds the shared local state-store fallback used by hosted
sessions and workflow checkpoints. Align its Azure Core and System
package dependencies to avoid assembly and downgrade conflicts.

* feat(foundry): use AgentServer state fallback

Use FoundryStateStore for sessions and workflow checkpoints in every
environment. Core beta.29 selects Foundry Storage when hosted and a
file-backed local store otherwise, so local runs exercise the production
storage shape without requiring Azure credentials.

Give the hosted workflow sample stable inner-agent identities so its
checkpoints remain compatible after container replacement.

* fix(hosting): harden durable state storage

Use published AgentServer packages so CI no longer depends on a local package source.

* build(hosting): scope AgentServer versions

Keep public package versions on their consumers so unrelated projects retain the central versions from main.

* build(hosting): use public AgentServer packages

Remove project overrides and keep package selection in the central catalog now that the required public releases are available.

* fix(hosting): preserve durable state identity

Keep keyed and default aliases on one session partition. Reject unstable unnamed direct-store usage and preserve live checkpoint branches during pruning.

* refactor(hosting): centralize hosted metadata

Carry storage identity through a hosting-specific agent wrapper, keep unknown middleware non-blocking at readiness, and align StateStore constructor parameter order.

* refactor(hosting): move session identity into store

* docs(hosting): explain session identity resolution

* fix(hosting): preserve protocol mismatch status

Reject unsupported protocol requests before AgentServer wraps handler failures in ResilientTaskException and converts the intended 501 response into a generic 500.
2026-08-20 20:38:04 +00:00
Korolev Dmitry e6617c407a .NET: Add Azure Blob Storage session persistence (#1893)
* setup azurestorage proj

* setup for azure blob as agentthreadstore

* add azurite as dependency for dotnet tests

* use services

* rollback

* azurite as a step

* move and rename

* renames / fixes

* rename to unit tests

* copilot changes

* .NET: Modernize Azure Blob session storage

Copilot-Session: 35e63850-1a85-4f7c-ac80-2274534c13b5

* .NET: Test hosted Blob session persistence

Copilot-Session: 35e63850-1a85-4f7c-ac80-2274534c13b5

* .NET: Address Azure Blob storage review feedback

Copilot-Session: 35e63850-1a85-4f7c-ac80-2274534c13b5

* .NET: Use default test target frameworks

Copilot-Session: 35e63850-1a85-4f7c-ac80-2274534c13b5

---------

Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Copilot-Session: 35e63850-1a85-4f7c-ac80-2274534c13b5
2026-08-20 18:30:35 +00:00
Ravi Kiran Pagidi cabb21a292 .NET: Clarify compaction provider and chat reducer choices (#7678)
* Document compaction provider and reducer choices

* Clarify chat history provider example

---------

Co-authored-by: Ravi Kiran Pagidi <236139898+ravikiranpagidi@users.noreply.github.com>
2026-08-20 14:01:46 +00:00
Dan Fiedler ab6c4d2dc8 Pin GitHub Actions to full-length commit SHAs (#7768) 2026-08-20 09:41:26 +00:00
Ruiming Zhao 2054d62702 Python: fix(github-copilot): forward telemetry config to client (#7625)
* fix(github-copilot): forward telemetry config to client

* Python: fix telemetry settings typing for github_copilot

`load_settings` does not coerce dict-typed fields, so GITHUB_COPILOT_TELEMETRY
and .env values reach the agent as plain strings. Declaring
`GitHubCopilotSettings.telemetry` as `dict[str, Any]` therefore misstated the
runtime contract and failed the test typing checks where a string is assigned.

Widen the annotation to `dict[str, Any] | str | None` and fix the union arm
resolution in `_check_override_type`: parameterized generics are not `type`
instances, so they were dropped from the allowed set and a valid dict override
was rejected at runtime. Arms without a runtime class, such as `Literal`, now
skip validation instead of narrowing it incorrectly.

Also drive the telemetry string tests through the documented environment
variable path rather than mutating `_settings` directly, and cover the
valid-JSON-but-not-an-object case.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 657d2953-4112-4a01-889b-c0c6863630c1

* Python: resolve settings override types through generic origins

Python 3.10 reports parameterized generics such as `dict[str, Any]` as
instances of `type`, so the union arm resolution kept the alias and
`isinstance` raised `TypeError: isinstance() argument 2 cannot be a
parameterized generic` on that interpreter.

Resolve every annotation through `get_origin` first via a shared
`_runtime_class` helper, which also removes the same latent failure for a
non-union parameterized generic field, and return `None` for annotations such
as `Literal[...]` that have no runtime class so validation is skipped rather
than narrowed incorrectly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 657d2953-4112-4a01-889b-c0c6863630c1

---------

Co-authored-by: Giles Odigwe <gilesodigwe@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 657d2953-4112-4a01-889b-c0c6863630c1
2026-08-20 08:33:46 +00:00
Yufeng He ecc1430977 Python: defer turn-scoped after_run providers to the agent loop boundary (#7289)
* Python: defer turn-scoped after_run providers to the agent loop boundary

Each AgentLoopMiddleware iteration is a full agent run, so CompactionProvider.after_run fired per iteration and rewrote persisted history mid-task (#7236). Providers can now opt into turn scope with after_run_once_per_turn; iterations defer them via a contextvar, and the loop fires them once at the boundary. CompactionProvider opts in; HistoryProvider keeps its incremental per-run persistence.

* Python: key loop suppression to the looping agent and pass run options through

Two review follow-ups: the contextvar now carries the agent instance so a nested agent.run() inside a loop iteration is not suppressed as if it were an iteration, and the boundary SessionContext forwards the original run options to turn-scoped providers.

* fix(core): carry the loop-iteration stamp in run options, not a contextvar

The contextvar marker leaked in two ways. Held across a streamed yield
it bled into the caller's context, suppressing turn-scoped providers on
an unrelated same-agent run while the stream was paused, and a reset
from a different consuming task raised on the token. Keyed to the agent
instance, it also swallowed the boundary flush of a nested loop on the
same agent with its own session.

Stamp the runs the loop drives through their options instead. Run
options reach only the inner runs (they never enter the model request),
a nested or concurrent run starts with fresh options and keeps its own
turn, and there is no token to reset, so stream consumption is safe from
any task.

* Python: annotate custom option keys in the after_run provider test

* fix: nosec the loop-iteration options key (bandit B105 false positive)

* Python: fix: suppress the loop-token key lint with ruff: ignore

* Python: fix: silence the two pyright private-usage flags the repo's own idiom covers

---------

Co-authored-by: Giles Odigwe <79032838+giles17@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-08-20 08:32:55 +00:00
westey f666102d0c Python: Harness blog part4 samples (#7698)
* Add harness blog post part 4 samples.

* Add harness sample fixes for python

* Point FileMemoryStore to home for hosted agents.

* Python sample fixes for toolbox

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3a5efc79-5c78-40b8-a1c5-c0f84e0795e1

* Address PR comments

* Python blog sample fixes

* Address PR comments

* Fix formatting

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3a5efc79-5c78-40b8-a1c5-c0f84e0795e1
2026-08-20 05:53:11 +00:00
ALEX LILUZ 26b28b4386 Python: Avoid unchanged AG-UI predictive state snapshots (#7766)
* Python: Avoid unchanged AG-UI predictive state snapshots

Only emit the coalesced snapshot when predictive updates were actually pending or a deterministic state update was returned.

Assisted-by: Codex <codex@openai.com>

* Python: Exercise the predictive update path in snapshot tests

Use the handler streaming API to create pending state and narrow snapshot events by their concrete type.

Assisted-by: Codex <codex@openai.com>
2026-08-20 05:51:13 +00:00
Evan Mattson 435201b71b Python: Fix A2A input handling in orchestrations (#7761)
* fix(a2a): reject empty invocations explicitly

Key decisions:
- Keep A2A continuation authority explicit; durable session task state only enriches diagnostics.
- Raise AgentInvalidRequestException with participant and available task context instead of inventing input.
- Leave AgentExecutor and Group Chat production contracts unchanged.

Files changed:
- packages/a2a/agent_framework_a2a/_agent.py
- packages/a2a/tests/test_a2a_agent.py
- packages/a2a/tests/test_a2a_group_chat.py

Notes for next iteration:
- No blockers. INPUT_REQUIRED pause/resume remains a separate task.

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

* fix(a2a): pause group chat for remote input

Key decisions:
- Translate A2A INPUT_REQUIRED task content into the existing Content user-input-request contract.
- Use the remote task ID as stable request correlation for streamed and finalized responses.
- Reuse AgentExecutor request handling so caller input resumes the same task without a workflow-specific A2A path.

Files changed:
- packages/a2a/agent_framework_a2a/_agent.py
- packages/a2a/tests/test_a2a_agent.py
- packages/a2a/tests/test_a2a_group_chat.py

Notes for next iteration:
- Checkpoint restoration of pending A2A input is now unblocked.
- The local issue file could not be moved because repository issue files are restricted by content exclusion policy.

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

* fix(a2a): restore pending input from checkpoints

Key decisions:
- Keep normalized INPUT_REQUIRED content durable by excluding transport-only protobuf raw representations.
- Restore through the existing AgentExecutor checkpoint and request-response path without a new schema or continuation API.
- Cover file-backed restoration in streaming and non-streaming Group Chat runs, including unrelated-response rejection and exact task resumption.

Files changed:
- packages/a2a/agent_framework_a2a/_agent.py
- packages/a2a/tests/test_a2a_group_chat.py

Notes for next iteration:
- The local issue file could not be moved because repository issue files are restricted by content exclusion policy.

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

* test(handoff): lock textless target context

Key decisions:
- Exercise the built Handoff workflow in streaming and non-streaming modes instead of bypassing routing, sessions, or termination.
- Keep the slice test-only because current production already carries the initial task to a textless handoff target without synthetic user input.
- Revisit the source to verify its handoff function call retains a matching result and user-turn termination sees only caller messages.

Files changed:
- packages/orchestrations/tests/test_handoff.py

Notes for next iteration:
- No production defect was reproduced.
- The local issue file could not be moved because repository issue files are restricted by content exclusion policy.

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

* test(handoff): use resolved IDs in event assertions

* fix(workflows): preserve A2A input request semantics

* fix(workflows): preserve input request correlation

* fix(a2a): deduplicate message-less input requests

* fix(workflows): preserve specialized input requests

* test(openai): use current web search model

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-19 23:55:01 +00:00
SergeyMenshykh d29e7be7fd .NET: Fix A2A streaming artifact updates (#7722)
* .NET: Fix A2A streaming artifact updates

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

Copilot-Session: d07945ad-4b76-48da-a499-a915c076cff9

* Flush buffered A2A artifacts on stream failure

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

Copilot-Session: d07945ad-4b76-48da-a499-a915c076cff9

* Aggregate A2A message streams incrementally

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

Copilot-Session: d07945ad-4b76-48da-a499-a915c076cff9

* Fix duplicate A2A message declaration

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

Copilot-Session: d07945ad-4b76-48da-a499-a915c076cff9

---------

Co-authored-by: SergeyMenshykh <SergeMenshikh@outlook.com>
Copilot-Session: d07945ad-4b76-48da-a499-a915c076cff9
2026-08-19 19:28:11 +00:00
King Star 26b9200c21 Python: Preserve AG-UI tool message IDs across snapshots (#7510)
* fix(ag-ui): preserve streamed tool message IDs

* fix(ag-ui): align approval and MCP tool message IDs

* fix(ag-ui): ensure unique tool segment IDs

* fix(ag-ui): keep tool and text snapshot IDs unique

---------

Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
2026-08-19 17:43:53 +00:00
Javier Calvarro Nelson e2938f4531 .NET: Remove AGUI history special cases (#7741)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-19 16:50:46 +00:00
SergeyMenshykh b1377fad52 .NET: Suppress Swagger UI CodeQL alert in sample (#7764)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b565023-c86e-496f-a1a1-be59b4d89cb7
2026-08-19 15:13:44 +00:00
Javier Calvarro Nelson 064751c5f3 .NET: Upgrade AG-UI SDK packages to 0.0.5 (#7742)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 425c405b-1fd3-4ba6-b332-a195598374b4
2026-08-19 10:53:56 +00:00
MohammadHaroonAbuomar 10bf8d7d9e .NET: agent-hooks interception contract as a first-class experimental feature (#7564)
* feat(dotnet): agent-hooks interception contract as an experimental package

Add Microsoft.Agents.AI.AgentHooks, implementing the AGENT-HOOKS-0.1
control contract on the framework's native decorator seams, mirroring
the merged Python feature (#7515) in .NET idiom:

- One public factory (CreateAIAgentWithAgentHooks, per-run and
  host-owned-session overloads) composes agent, chat and function
  seams as one indivisible unit; the seam decorators are internal, so
  partial installs are impossible by construction.
- All eight interception points: input/output at the agent seam,
  pre/post_model_call below the function-invocation loop (every model
  service call bracketed individually), pre/post_tool_call via the
  function-invocation middleware seam, agent_startup/agent_shutdown
  bracketing each run.
- Fail-closed enforcement throughout: transforms write back into the
  native messages/arguments/results or throw; rich content is
  preserved as AIContent objects; interceptor crashes surface as
  host_error denies; enforcement-layer failures halt the run through
  FunctionInvocationContext.Terminate (the loop's only loud escape).
- Streaming is fully buffered per spec buffered_output semantics: a
  deny releases zero updates; transformed responses re-derive the
  released updates so egress never diverges from verdicted content.
- Verdict-before-durability: end-of-run history and context-provider
  writes defer behind the output verdict via gating provider wrappers
  (flushed post-transform with verdicted-message substitution for
  streams, dropped on deny); per-service-call persistence sits above
  the chat seam and is covered by its own post_model_call verdict;
  per-run history-provider overrides in run options are wrapped too;
  nested guarded sub-agents persist inline at their own boundaries.
- Opt-in dependency: ResponsibleAI.AgentHooks 0.1.0-alpha.4 (bundles
  native runtimes) referenced only by the new package; no existing
  framework source is modified.
- 58 tests: deny-before-execution and transform write-back per seam,
  rich-content preservation, streaming ordering with zero egress on
  deny, error bracketing, concurrency isolation, host-owned sessions,
  evaluate_only, approval-seam lift, persistence gating, misuse
  fail-closed paths, and codec units.

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

* fix(dotnet): close structural bypasses at the ChatClientAgent boundary

Address both reviewers' probe-confirmed findings; the runtime
enforcement held everywhere, every fix is at the structural boundary:

- Gate the implicit default ChatHistoryProvider: with no provider
  configured, ChatClientAgent creates an InMemoryChatHistoryProvider
  the factory never saw, so denied output became durable session
  history and replayed to the model on the zero-config path (both
  stream modes). The factory now materializes and gates the default,
  setting the history-conflict flags to mimic implicit-default
  semantics for service-managed-history agents.
- Wrap per-run provider overrides on BOTH dictionaries: base
  AgentRunOptions.AdditionalProperties is merged into the chat options
  with precedence, so a base-level override bypassed (and displaced)
  the wrapped ChatOptions-level entry. Plain AgentRunOptions is
  covered too, and the wrap is copy-on-write — the caller's options
  and dictionaries are never mutated.
- Reject per-run ChatClientFactory on guarded agents (fail closed): it
  would replace the guarded chat pipeline and the tool-wrapping stage
  riding it, silently removing the chat and tool seams.
- Reject a supplied client already containing a
  FunctionInvokingChatClient: it would execute tools below the chat
  seam, before any post_model_call verdict and outside the tool seam.
- Run wire projections inside the guarded blocks at the chat and
  function seams: a poisoned value whose serialization throws now
  fails the run closed (function seam: host_error halt; chat seam:
  gated persistence refused before the failure propagates).
- Suppress provider failure notifications once a run-level deny or
  halt stands, so the denied turn's request messages never reach
  provider code.
- Document the deferred-OpenTelemetry observer channel (request-side
  spans capture pre-transform content under sensitive-data telemetry).
- Rename the factory to AsAIAgentWithAgentHooks per repo convention.

10 new boundary regression tests mined from the review probes
(default-provider durability in both stream modes with session-replay
assertions, both override dictionaries incl. the displacement shape,
plain-run-options override, copy-on-write, factory and supplied-FICC
rejections, poisoned-projection fail-closed); 68 total, all green.

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

* fix(dotnet): redact denied-run failure notifications for both provider kinds

The deny/halt handling of provider failure notifications only covered
the chat-history wrapper; a context provider still received the denied
turn's request messages on its failure notification. Both gating
wrappers now REDACT instead of suppress: the notification is forwarded
with empty request messages and the original exception, preserving the
documented failure-cleanup contract (providers releasing per-run
resources on the failure signal keep working) while the denied turn's
request messages never reach provider code.

Regression tests assert both provider kinds receive the redacted
notification (zero request messages) on a denied run and full
notifications on ordinary, verdict-free failures. 70 tests total.

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

* fix(dotnet): address Copilot review on the agent-hooks PR

- Run options: always clone chat-typed run options (the framework's
  function-invocation middleware chains its per-run factory onto the
  instance it receives, so forwarding the caller's instance leaked
  that factory into it — reuse tripped the rejection, concurrent
  reuse raced), and recognize the framework middleware's own factory
  as legitimate: it wraps the guarded pipeline (tool rewriting), so
  outer function-middleware composition now works, while its chained
  factories are walked so a caller-supplied factory cannot ride in
  unnoticed.
- Streaming: re-derived (transformed) updates preserve the response's
  ContinuationToken (ToAgentResponseUpdates does not project it), so
  transformed background streaming responses remain resumable; a
  message-less response releases a metadata-only update carrying it.
- Codecs: transformed tool calls are validated for complete shape and
  uniqueness before reconciliation (non-empty string id and name,
  object-valued args, distinct ids) — malformed shapes fail closed
  instead of becoming invalid native calls. Deliberately stricter
  than the merged Python codec, which coerces added-call shapes.
- Role defaulting in message write-backs is confirmed exact Python
  parity (user/assistant defaults per the merged codecs) and is now
  locked by tests rather than changed.
- ADR 0035 records the seam order, persistence gating, fail-closed
  behavior, alternatives and known limitations.

14 new tests (options reuse, outer function-middleware composition,
smuggled-factory rejection, continuation-token preservation, 8
malformed tool-call shapes, 2 role-default parity); 84 total, green.

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

* feat(dotnet): project the per-call tool set on pre_model_call emissions

Context providers can register additional tools during run preparation,
after agent_startup has been emitted, so tools_registered is inherently
a run-start snapshot and can be a partial view of the tools eventually
offered to the model.

- Emit the spec's optional pre_model_call tools field ({name,
  description?}) from the per-call effective ChatOptions.Tools — the
  completed set for each call, including provider-added tools.
- Document tools_registered as the run-start snapshot on the agent
  seam (dynamic registrations surface per call and are bracketed by
  the tool seam when invoked).
- Probe-confirm enforcement completeness for provider-added tools:
  they flow through the guarded pipeline's tool-wrapping stage, emit
  pre/post_tool_call, and a pre_tool_call deny blocks their
  invocation exactly like constructor-registered tools.

Two new tests (bracketing + audit projections, deny-blocks); 86
total, green.

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

* refactor(dotnet): one artifact per file; rewrap foreign gating wrappers

Per review:
- Split the three multi-type files (AgentHooksGatingProviders.cs,
  AgentHooksRunState.cs, AgentHooksWireCodecs.cs) into one type per
  file, file name matching the type name, per repo convention. No
  behavior changes; namespaces and access levels unchanged.
- Close a validation asymmetry at the provider gate: the per-run
  override wrap skipped any gating wrapper, including one owned by a
  DIFFERENT agent-hooks installation — which runs inline under this
  run's state (its own gate is not covering here), so a denied run's
  history could persist straight through it. Overrides are now
  re-wrapped unless the wrapper belongs to this installation
  (reference-equal configuration). The provider seam's inline
  behavior for foreign/absent state is otherwise deliberate: inline
  is the safe direction there (content of unguarded or differently
  guarded runs is covered by its own verdicts or none), and throwing
  would break the legitimate double-wrap flush flow.

One new regression test (foreign wrapper as per-run override on a
denied run persists nothing); 87 total, green.

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

* refactor(dotnet): accept params IEnumerable for agent-hooks interceptors

Per review: the constructor only iterates the interceptors, so widen
the parameter from params IInterceptor[] to the C# 13 params
IEnumerable<IInterceptor>. The sequence is enumerated exactly once
into the internal registration list (sequences may be
single-enumeration); per-item null validation and the factory's
at-least-one-interceptor check are unchanged, and an explicit null
sequence now throws ArgumentNullException. Params-form call sites are
source-compatible.

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

* build(dotnet): ship Microsoft.Agents.AI.AgentHooks as a preview package

Per maintainer review on the PR:

- Add the project to agent-framework-release.slnf and import the
  shared packaging props so the package ships. Version follows the
  repo default for unmarked packages (preview suffix), matching the
  package's [Experimental] surface and alpha upstream dependency:
  1.17.0-preview.<date>.1.
- Package metadata: sibling-style title, fuller description, tags;
  shared icon and NUGET.md readme via the packaging props. Verified
  dotnet pack locally: ResponsibleAI.AgentHooks 0.1.0-alpha.4 flows
  as a normal dependency and the project references become 1.17.0
  package dependencies.
- Update ADR 0035: shipping as preview per maintainer decision
  replaces the build-only-pending-maturity stance.

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

* build(dotnet): version the agent-hooks package as alpha

Per maintainer review: the package's maturity marker follows the
ResponsibleAI.AgentHooks dependency it is built on (alpha), rather
than the repo's default preview suffix. Packs as
1.17.0-alpha.260804.1; ADR 0035 updated.

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

* refactor(dotnet): group agent-hooks internals into Core and Codecs folders

Per review: only the public surface (the factory extensions and
options) stays at the project root; the internal seam decorators, run
state and gating providers move to Core/, and the wire projection
codecs to Codecs/. Pure file moves — namespaces stay flat per the
core package's folder convention (ChatClient/, Memory/); no content
changes.

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

* docs(dotnet): clarify session scoping and name the sessionId argument

Per review:
- Name the AgentContextBuilder arguments at the run-state factory so
  the GUID reads as what it is (the per-run agent-hooks session id).
- Document both branches of CreateRunState: session-scoped means the
  host owns the emitter/builder and the session boundaries (one
  session spanning runs, no agent_startup/agent_shutdown emitted by
  the agent); the default is one session per run with a fresh
  emitter, fresh sequence and isolated record trail, which is what
  keeps concurrent runs' emissions from interleaving.

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

* fix(dotnet): harden agent-hooks factory and input projection per review

- Input projection returns (payload, content, role) as one typed
  result so the emission site never re-reads payload properties by
  name: the both-fields-exist invariant holds by construction. (The
  previous reads were fail-closed even hypothetically — JsonObject's
  indexer yields null, and a null content is rejected by the SDK's
  envelope validation — but reading back what we just produced was
  needlessly fragile-looking.)
- Reject UseProvidedChatClientAsIs on the factory: it signals a fully
  custom, do-not-touch client stack, which is incompatible with a
  factory whose job is to decorate the supplied client and rely on
  the agent's default pipeline above the chat seam. Honoring it would
  silently change where (and whether) the seams sit.
- Log swallowed agent_shutdown emission failures (logger resolved the
  same way the agent resolves its own: services, then the chat
  client, then null) so incomplete session trails are trackable;
  OutOfMemoryException stays unswallowed. The swallow remains
  correct: the run's own outcome is already propagating and the
  trail closure is best-effort by contract.

89th test: UseProvidedChatClientAsIs rejection.

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

* build(dotnet): attribute the Agent-Hooks protocol in the package identity

Per review:
- Title per suggestion: 'Microsoft Agent Framework - Responsible AI
  Agent-Hooks Protocol Support'; description names the protocol
  precisely (AGENT-HOOKS-0.1, maintained by the Responsible AI
  project at github.com/responsibleai/agent-hooks) so the package
  reads as protocol support, not a MAF-owned feature; tags aligned.
- Drop the [Experimental] attributes: per repo convention the
  attribute gates unstable surface inside released packages
  (Harness, core), while pre-release packages (Valkey and Mcp at
  alpha, Mem0 and LocalCodeAct at preview) carry none — the version
  suffix is the maturity signal.
- Drop the describing comment on the central package version entry.

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

* refactor(dotnet): split agent-hooks test fixtures into Support files

Per review: one type per file under Support/ (mock client, guards,
recording providers, helpers), matching the src-side convention; pure
mechanical split, flat namespace.

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

---------

Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>
2026-08-19 10:48:13 +00:00
Giles Odigwe 8be7c93063 Python: Preserve structured instructions when merging chat options (#7730)
* Python: Preserve structured instructions when merging chat options

`instructions` is declared as `str` on `ChatOptions`, but chat clients may widen it
to a provider-native structured form. Three merge paths combined it with an f-string,
which coerced any non-string value to its `repr`, turning structured metadata into
literal text before any client could see it:

- `merge_chat_options` (`_types.py`)
- `_merge_options` (`_agents.py`, agent defaults + per-run options)
- provider-contributed instructions in `_prepare_session_and_messages` (`_agents.py`)

The last of these is the reported case: once any context provider (for example
`SkillsProvider`) contributes instructions, structured instructions were replaced by
their `repr`, so the model received Python dict syntax as its system prompt and
Anthropic prompt caching silently stopped working.

Add a shared `_append_instructions` helper that concatenates strings as before and
otherwise extends element-wise, always appending so the leading portion stays
unchanged for providers that treat it as a stable, structure-sensitive prefix. A lone
mapping is treated as a single element rather than iterated into its keys.

On the Anthropic side, `_extract_structured_instructions` now normalizes bare strings
into text blocks, since appended instructions arrive alongside caller-supplied blocks.

Fixes #7700

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f428b149-a306-484a-b423-4e9de69f0b90

* Python: address review feedback on structured instructions fix

Parameterize the Anthropic regression test over both the with- and
without-SkillsProvider configurations so the structure-preserving behavior
is asserted in the baseline case too.

Normalize structured instructions in `_get_instructions_from_options` so
telemetry records the instruction text for provider-native block shapes,
extracting only `text` values to keep provider metadata out of spans.

Use `cast` for the structured `default_options` in both regression tests so
the test type checkers resolve the client options type correctly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f428b149-a306-484a-b423-4e9de69f0b90

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Copilot-Session: f428b149-a306-484a-b423-4e9de69f0b90
2026-08-19 10:25:54 +00:00
Evan Mattson c527d61ac7 Update Python codeowners (#7762) 2026-08-19 10:00:30 +00:00
Manjunath Janardhan ec407cf56f Python: fix: preserve Agent additional_properties in HandoffBuilder clones (#7755)
HandoffAgentExecutor clones each participant agent to attach handoff
tools, but the clone rebuilt the Agent without forwarding
additional_properties, so middleware and integrations observing
context.agent.additional_properties during handoff runs saw an empty
dict while the original agent retained its configuration.

Pass a deepcopy of the original agent's additional_properties into the
clone so handoff-executed agents keep their configured metadata and the
original agent stays untouched.

Fixes #7750
2026-08-19 09:57:32 +00:00
Roger Barreto 0f583ec8a3 .NET: Migrate remaining Foundry hosted samples to source deployment (#7668)
* .NET: Migrate 6 hosted-agent samples to source (ZIP) deploy

Extend the source (ZIP) deploy pattern established for Hosted-ChatClientAgent to Hosted-LocalTools, Hosted-Workflow-Simple, Hosted-TextRag, Hosted-Observability, Hosted-Files and Hosted-FoundryAgent. Each gains an azure.yaml (codeConfiguration/remote_build, ASPNETCORE_URLS, model env) and the canonical .agentignore, a self-contained csproj (single target, CPM opt-out, explicit published package versions, AgentFrameworkVersion), a Program.cs that drops the shared contributor scaffolding for DefaultAzureCredential, an updated .env.example and README, and drops the container-mode files (agent.yaml, agent.manifest.yaml, Dockerfile, Dockerfile.contributor). LocalTools, Workflow-Simple, TextRag, Observability and Files were verified deploying live via remote_build; Workflow-Simple returns a workflow runtime error at invoke that is unrelated to the deploy mode.

* .NET: Migrate Hosted-Invocations-EchoAgent and Hosted-LocalCodeAct to source (ZIP) deploy

EchoAgent (Invocations protocol) and LocalCodeAct migrated to the zip/code-deploy pattern (azure.yaml, .agentignore, self-contained csproj, README, container files removed). EchoAgent maps /readiness explicitly because the Invocations SDK does not auto-map it. Both verified live via remote_build on a Foundry project; LocalCodeAct's execute_code ran server-side (compute 21+21 -> 42).

* .NET: Migrate remaining hosted-agent samples to source (ZIP) deploy

Migrate Hosted-McpTools, Hosted-MemoryAgent, Hosted-AgentSkills, Hosted-AzureSearchRag, Hosted-Toolbox, Hosted-Toolbox-AuthPaths and Hosted-ToolboxMcpSkills to the zip/code-deploy pattern (azure.yaml with codeConfiguration + sample-specific env passthrough, canonical .agentignore, self-contained csproj, Program.cs dropping the shared contributor scaffolding for DefaultAzureCredential, updated .env.example and README, container files removed). Also restore the Hosted-Invocations-EchoAgent csproj filename the solution references. McpTools verified live via remote_build against the public Microsoft Learn MCP server; the memory/search/toolbox/skills samples build locally and deploy via remote_build but need their external resources (memory store, search index, toolbox connections, skills) provisioned to exercise end to end.

* .NET: Migrate Hosted-Workflow-Handoff to source (ZIP) deploy

Migrate the triage handoff workflow sample to the zip/code-deploy pattern (azure.yaml with codeConfiguration and Azure OpenAI env passthrough, canonical .agentignore, self-contained csproj using AgentFrameworkVersion for Foundry/Foundry.Hosting/Hosting, Program.cs dropping the shared contributor scaffolding for DefaultAzureCredential, updated .env.example and README, container files removed). Builds via remote_build; live needs an Azure OpenAI resource (AZURE_OPENAI_ENDPOINT/AZURE_OPENAI_DEPLOYMENT).

* .NET: Copy Hosted-AgentSkills skills/ into build output

The startup provisioning helper reads SKILL.md files from AppContext.BaseDirectory/skills, but the project did not copy the skills/ folder to the build/publish output, so at runtime the source directory did not exist and provisioning was silently skipped. Add a Content include (PreserveNewest), matching the resources/ pattern already used by Hosted-Files.

* .NET: Suppress OPENAI001 in Hosted-Workflow-Handoff for standalone ZIP build

The repo-wide Directory.Build.props suppresses OPENAI001, but that file does
not travel in the code/ZIP deploy package. The standalone dotnet publish the
Foundry code deploy runs then fails with error OPENAI001 on the experimental
GetResponsesClient().AsIChatClient() call. Add OPENAI001 to the project NoWarn
so the sample builds in the code-deploy pipeline, matching SimpleAgent.csproj.

* .NET: Document live-verified idiosyncrasies in Foundry hosted sample READMEs

Align every FoundryHostedAgents sample README with the documented azd flow and
add the idiosyncrasies found while live-testing each sample on a Foundry project:

- All samples: 'azd down' reports success but does not delete the hosted agent;
  document the explicit REST DELETE needed to remove it.
- Hosted-Workflow-Handoff: it builds its own AzureOpenAIClient (data-plane), so
  the agent identity needs the 'Cognitive Services OpenAI User' role on the
  Azure OpenAI account. azd only grants 'Foundry User' on the project, so add a
  step to grant the data-plane role and explain the triage-step failure without it.
- Hosted-Toolbox / Toolbox-AuthPaths / ToolboxMcpSkills: the toolbox must already
  exist and the agent identity must be able to read it; toolboxes with OAuth-gated
  tools return an oauth_consent_request and response.incomplete on first invoke.

* .NET: Address Foundry hosted sample review feedback

Make sample configuration reject blank azd substitutions and document every required environment value inside the scaffolded project flow.

Separate the hosted endpoint name from the Foundry managed prompt-agent name, fix standalone MemoryAgent diagnostics, and complete the contributor local package feed for Hosting, LocalCodeAct, and MCP.

Use azd for agent invocation and az rest for authenticated administration without exposing tokens. Add native MCP approval handling to the toolbox consent client and make its local path target the standard responses endpoint.

Validated all changed samples locally, the contributor flow in PowerShell and Bash, and the supported live scenarios on the TAO cace project.

* .NET: Fix advanced hosted sample project access

Document and validate the Foundry User grant required by hosted version identities that access project data plane APIs.

Add the Skills preview feature header and use a writable temporary directory for downloaded skills because source deployments mount the application directory read only.

Update AgentSkills, MemoryAgent, FoundryAgent, and ToolboxMcpSkills deployment guides with the post deploy identity grant. All four scenarios passed live on the TAO cace project.
2026-08-19 09:43:14 +00:00
Daniel Roth 9917bddc2b .NET: Update AG-UI samples for latest MAF + AG-UI SDK and align with docs (#7295)
* Simplify AG-UI Step04 human-in-the-loop sample to idiomatic pattern

The Step04 sample previously wrapped both the server and client agents in
custom ServerFunctionApproval*Agent middleware (~470 lines across two files)
to marshal a bespoke approval protocol over AG-UI. This is no longer needed:
MapAGUIServer natively emits the tool-approval interrupt when the model calls
an ApprovalRequiredAIFunction, and AGUIChatClient natively transports the
client's ToolApprovalResponseContent decision back to resume the run.

Changes:
- Server: map the ChatClientAgent directly with MapAGUIServer; remove the
  ServerFunctionApprovalAgent wrapper, the JsonOptions plumbing, and the
  ApprovalJsonContext registration.
- Client: use the AGUIChatClient-backed agent directly; the existing loop
  already handles ToolApprovalRequestContent -> CreateResponse idiomatically.
- Delete ServerFunctionApprovalServerAgent.cs and
  ServerFunctionApprovalClientAgent.cs.

Verified end-to-end (approval request -> approve -> tool executes -> final
response) against GitHub Models. Both projects build with 0 warnings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eeb2168d-2ecc-4f8d-9830-c287072eb7e1

* Update AG-UI Step04 README to describe native approval flow

The Step04 human-in-the-loop sample no longer uses the custom ServerFunctionApprovalServerAgent / ServerFunctionApprovalClientAgent wrappers. Update the README so it describes the idiomatic native flow: the server maps a plain agent with MapAGUIServer and relies on ApprovalRequiredAIFunction to raise the approval interrupt, and the client handles ToolApprovalRequestContent and replies with ToolApprovalResponseContent.

* Fix AG-UI Step04 README server port to match client default

The Step04 client defaults to http://localhost:5100 (and the server launchSettings also uses 5100), but the README told users to run the server on port 8888, so the client could not reach it. Align the Step04 server run command to 5100. Other steps intentionally keep 8888 because their clients default to that port.

* Update AG-UI .NET samples for latest MAF + AG-UI SDK and align with docs

- Bump AGUI.* packages 0.0.3 to 0.0.4 (Directory.Packages.props)
- Step01/02/03: drop AddHttpClient().AddLogging() server noise and simplify the
  client run-started output to match the getting-started doc (no thread plumbing)
- Step04 (HITL): remove HTTP body logging and MEAI001 pragmas, give the approval
  tool an explicit name, and align the resume decision message with the doc
- Step05 (state): replace the custom SharedStateAgent/StatefulAgent DataContent
  pattern (dropped by released AGUI.Server) with declarative
  AGUIStreamOptions.MapResultAsStateSnapshot plus a thin RecipeStateAgent that
  reads RunAgentInput.State, and align the Recipe models with the docs
- Refresh README to the shipped API (MapAGUIServer, ApprovalRequiredAIFunction,
  declarative state)

Verified: all 10 sample projects build; Step04 approval/resume and Step05 state
snapshot round-trip run end-to-end against GitHub Models.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eeb2168d-2ecc-4f8d-9830-c287072eb7e1

* Name the Step02 backend tool search_restaurants to match the docs

Give the SearchRestaurants tool an explicit "search_restaurants" name so the
client displays an accurate tool name (not a compiler-mangled local-function
name) and stays aligned with the backend-tool-rendering doc.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eeb2168d-2ecc-4f8d-9830-c287072eb7e1

* Add UTF-8 BOM to Step05 sample files to satisfy check-format

The check-format CI job enforces the repository's utf-8-bom charset rule via
dotnet format. The Step05 files added in this PR were saved without a BOM.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eeb2168d-2ecc-4f8d-9830-c287072eb7e1

* Fix AG-UI sample conversation history

Let AgentSession own prior messages so clients send only each new turn, and give the frontend location tool a stable protocol name.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Copilot-Session: eeb2168d-2ecc-4f8d-9830-c287072eb7e1
2026-08-19 09:41:31 +00:00
pratik wayase da11daebe5 Python: fix: prevent superlinear history growth by deduplicating messages in save_messages (#7242)
* fix: prevent superlinear history growth by deduplicating messages in save_messages

* fix: address review feedback for history deduplication

* fix: Prevent superlinear history growth by deduplicating messages

* fix: add list[Message] type hints

* fix(sessions): resolve deduplication churn and collapsing of identical message

* fix(sessions): replace uuid/seen-set dedup with sequence aware filtering

* fix: use forward-scan sequence alignment in filter_new_messages

* fix(core): annotate new_msgs type to resolve pyright errors

---------

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-08-19 04:40:16 +00:00
NekoPunch e74ac4613c fix(python): coerce JSON workflow resume payloads (#7684)
AG-UI clients send plain JSON, but structured response types were only
accepted as already-built instances, and core's coercion stopped at the
outer object, letting raw dicts sit inside typed fields. Coercion now
walks declared annotations and returns the input untouched whenever it
cannot satisfy them.
2026-08-19 04:20:24 +00:00
Evan Mattson 1f738cdeb7 .NET: Python: Clarify PR review comment resolution (#7746)
* Clarify PR review comment resolution

* Sync PR review resolution guidance
2026-08-18 23:27:10 +00:00
Evan Mattson e6536fb459 Python: Align AG-UI run continuity (#7662)
* Python: Align AG-UI run continuity

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

Copilot-Session: aadad05d-2646-405f-8c62-a7a223abfc92

* Python: Refine AG-UI continuation ownership

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

Copilot-Session: aadad05d-2646-405f-8c62-a7a223abfc92

* Python: Persist AG-UI checkpoint ownership

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

Copilot-Session: aadad05d-2646-405f-8c62-a7a223abfc92

---------

Copilot-Session: aadad05d-2646-405f-8c62-a7a223abfc92
2026-08-18 22:53:09 +00:00
MohammadHaroonAbuomar 58da0cc253 Python: add MiddlewareFailure, a first-class fatal signal for function middleware (#7562)
* feat(core): first-class fatal signal (MiddlewareFailure) for function middleware

The function-invocation loop converts every exception raised by
function middleware into a tool-error result and keeps looping, so
middleware that needs fail-closed semantics (enforcement layers,
guardrails) had no loud escape: the agent-hooks feature simulated one
by mutating shared run state, raising MiddlewareTermination, and
re-raising the real failure two hops away at the run boundary.

Introduce MiddlewareFailure (a MiddlewareException sibling of
MiddlewareTermination) as the loop's explicit fail-closed escape:

- _auto_invoke_function re-raises it (both the direct and the
  pipeline path) instead of absorbing it into a tool-error result;
  ordinary exceptions keep the absorb-and-continue contract.
- A failing call fails the whole parallel batch: in-flight sibling
  tool tasks are cancelled and awaited before the failure propagates.
- Every existing MiddlewareTermination absorb site (agent/chat
  pipelines, _execute_single_function_call, harness loop, purview)
  passes it through untouched by construction, and agent/chat
  middleware exceptions already propagate, so one exception type
  gives uniform fail-loud semantics across all three categories.

Migrate the agent-hooks feature to the new signal: delete the
_RunState.halted back-channel and its three run-boundary re-raise
checks, drop the halted arm of the termination special case in the
function middleware (the approval-request pass-through moves to the
single approval check on the normal path), and fail partial installs
loudly. Tool-seam host_error blocks keep surfacing as
InterceptionBlocked at the run boundary via the exception cause chain
(one deny surface at every seam, pinned by tests).

Spec 004 gains the middleware-failure invariants and matrix rows.

Closes #7522

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

* fix(core): harden tool-seam unwrap and pin review findings

Review round follow-ups for the MiddlewareFailure feature:

- Only agent-hooks' own tagged tool-seam halts (_ToolSeamBlockFailure)
  authorize re-raising the chained InterceptionBlocked at the run
  boundary; a third-party MiddlewareFailure with a crafted
  InterceptionBlocked cause now propagates as raised instead of
  laundering an attacker-shaped interception record into the feature's
  deny surface (regression test added, verified by mutation).
- Document that middleware must not catch MiddlewareFailure (docstring
  and spec 004): swallowing it converts a fail-closed abort back into
  a running, possibly unguarded loop.
- Pin the trailing termination re-raise in the agent-hooks function
  middleware: an inner short-circuit is bracketed and still propagates,
  skipping outer middleware post-code (test fails with the re-raise
  removed).

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

* fix(core): acyclic tool-seam unwrap chain; document cooperative batch cancellation

Address two automated-review findings on the MiddlewareFailure PR,
both confirmed empirically:

- _reraise_tool_seam_block created a two-object exception-chain cycle
  (block.__cause__ -> wrapper -> block) by re-raising the chained
  InterceptionBlocked `from` its transport wrapper. Detach the
  wrapper's back-links and re-raise bare, recording the wrapper as
  the block's __context__ — acyclic, both exceptions still visible in
  tracebacks. Regression test walks the chain and pins finiteness
  (verified to fail against the cyclic re-raise).

- Batch cancellation is cooperative: a synchronous tool body already
  running in a worker thread (asyncio.to_thread) cannot be interrupted
  by task cancellation and may complete its side effects after the
  failure reached the caller; its result is discarded either way and
  propagation is not delayed behind it. Narrow the stated contract
  (MiddlewareFailure docstring, loop comment, spec 004) and pin it
  with a blocking-sync-sibling regression test.

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

* fix(core): settle dangling calls on service-managed conversations on abort

Address maintainer review on the MiddlewareFailure PR:

- A MiddlewareFailure escaping a tool batch on a service-managed
  conversation left the hosted thread ending in unresolved
  function_call items: _update_continuation_state persists
  session.service_session_id when the model turn completes (before
  tool execution), and probe-verified the next run sends only the new
  user message against that conversation — OpenAI-style continuations
  reject such a request, so a routine policy abort left the session
  permanently stuck. Both loops now settle the thread before
  propagating: one error function_result per dangling call, submitted
  with tool_choice="none" in a single extra request whose response is
  discarded; a settlement failure never masks the abort, and runs
  without a service-managed conversation make no extra request.
  Pinned by three regression tests (non-streaming, streaming, and the
  no-conversation no-cost case); spec 004 and the MiddlewareFailure
  docstring updated.

- Make the three tool-bracket escape tuples in the agent-hooks
  function middleware identical (MiddlewareTermination,
  MiddlewareFailure, CancelledError): a MiddlewareFailure raised
  inside the post/error-bracket emit bodies is unreachable today, but
  the uniform tuples remove the need to reason about why they would
  differ, and preserve the exact exception (including the private
  tool-seam tag) if the emitter ever surfaces one.

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

* fix(core): advance settled continuation; settle approved-replay aborts

Address maintainer review on the MiddlewareFailure settlement path,
both probe-verified (branch rebased onto current main first):

- Advance the persisted continuation to the settlement response. For
  response-ID continuations (OpenAI Responses store=True, where the
  response id is the continuation handle) the settlement response is
  the first endpoint whose chain includes the synthetic tool outputs;
  leaving session.service_session_id on the pre-settlement response
  made the settlement ineffective — the next run would continue from
  the still-unresolved turn. The settlement response now runs through
  _update_function_invocation_continuation_state (a no-op for stable
  conversation-object ids). Pinned by a regression test that fails
  with the advance removed.

- Cover the approval-resolution phase: a MiddlewareFailure raised
  while an approved tool is replayed escapes loudly (probe-verified,
  already the case) but executed before the loops' settlement seams,
  leaving the original — already service-persisted — call unresolved.
  _resolve_approval_responses now takes a settle_dangling_calls
  callback invoked with the approved batch on abort; the settlement
  helper became a layer method taking explicit calls
  (approval-response wrappers unwrap to their underlying calls,
  hosted-tool approvals are left to their provider protocol) and
  carries its own best-effort containment. Pinned by deny-during-
  replay regression tests in both response modes, mutation-verified.

Spec 004 invariants and matrix rows updated accordingly.

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

---------

Signed-off-by: MohammadHaroonAbuomar <40180927+MohammadHaroonAbuomar@users.noreply.github.com>
2026-08-18 22:33:52 +00:00
Giles Odigwe 2213ef8493 Add es-metadata.yml for Engineering System inventory (#7740)
Registers the repository with Engineering System inventory via the
InventoryAsCode provider, mapping it to its Service Tree service and
routing compliance work items to the owning team.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 05291438-8e37-49d6-84b6-5ffb7814abb8
2026-08-18 18:20:13 +00:00
westey f330457042 .NET: Pass IServiceProvider to ChatClientAgent in AddAIAgent overloads (#7737)
All four AddAIAgent overloads in AgentHostingServiceCollectionExtensions
created a ChatClientAgent without forwarding the IServiceProvider, so the
FunctionInvokingChatClient in the agent's pipeline had no service provider
and tools could not resolve their dependencies at invocation time.

Fixes #4453

Co-authored-by: Max Montes Soza <max-montes@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-18 18:19:01 +00:00
Copilot e33e78127f .NET: Fix snake_case argument names in Harness file tool descriptions (#7731)
* Initial plan

* Fix snake_case argument names in Harness file tool descriptions

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: westey-m <164392973+westey-m@users.noreply.github.com>
2026-08-18 18:05:09 +00:00
westey 4be584cc53 .NET: Add session-persisted chat client routing (#7641)
* Add RoutePersistingRoutingChatClient

* Address PR comments

* Address PR comments
2026-08-18 17:45:43 +00: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
697 changed files with 42683 additions and 6865 deletions
+26 -26
View File
@@ -49,43 +49,43 @@
/python @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU
# Python packages
/python/packages/a2a/ @chetantoshniwal @giles17 @eavanvalkenburg
/python/packages/a2a/ @chetantoshniwal @giles17 @eavanvalkenburg @moonbox3
/python/packages/ag-ui/ @chetantoshniwal @moonbox3 @giles17
/python/packages/anthropic/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/anthropic/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/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/azure-cosmos/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/azure-cosmos-memory/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/bedrock/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/chatkit/ @chetantoshniwal @moonbox3 @giles17
/python/packages/claude/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/copilotstudio/ @chetantoshniwal @eavanvalkenburg @giles17
/python/packages/claude/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/copilotstudio/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/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/core/agent_framework/_harness/ @chetantoshniwal @westey-m @eavanvalkenburg @moonbox3
/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/foundry_hosting/ @chetantoshniwal @TaoChenOSU @eavanvalkenburg @moonbox3
/python/packages/foundry_local/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/gemini/ @chetantoshniwal @giles17 @eavanvalkenburg @moonbox3
/python/packages/github_copilot/ @chetantoshniwal @giles17 @eavanvalkenburg @moonbox3
/python/packages/hosting/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU @moonbox3
/python/packages/hosting-a2a/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU @moonbox3
/python/packages/hosting-mcp/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU @moonbox3
/python/packages/hosting-responses/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU @moonbox3
/python/packages/hosting-telegram/ @chetantoshniwal @eavanvalkenburg @TaoChenOSU @moonbox3
/python/packages/hyperlight/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/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/mem0/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/mistral/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/monty/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/ollama/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/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
/python/packages/purview/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/redis/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
/python/packages/tools/ @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3
# Core .NET developers: @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
@@ -113,6 +113,7 @@
/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.AzureStorage/ @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
@@ -127,4 +128,3 @@
/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
+1 -1
View File
@@ -59,7 +59,7 @@ runs:
id: azure-login
if: ${{ (inputs.mode || 'app-with-fallback') != 'pat' }}
continue-on-error: true
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ inputs.azure-client-id }}
tenant-id: ${{ inputs.azure-tenant-id }}
@@ -24,7 +24,7 @@ runs:
using: "composite"
steps:
- name: Set up Node.js environment
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
@@ -59,7 +59,7 @@ runs:
sample-playbooks-${{ github.job }}-
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ inputs.azure-client-id }}
tenant-id: ${{ inputs.azure-tenant-id }}
+2
View File
@@ -58,3 +58,5 @@ updates:
schedule:
interval: "weekly"
day: "sunday"
cooldown:
default-days: 7
+13
View File
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
/**
* Check whether a comment contains only the DevFlow review command.
*
* @param {unknown} body - Issue comment body from the GitHub event payload.
* @returns {boolean} Whether the normalized comment is exactly `/review`.
*/
function isReviewCommand(body) {
return typeof body === 'string' && body.trim() === '/review';
}
module.exports = isReviewCommand;
+6 -3
View File
@@ -74,9 +74,12 @@ code before the user has reviewed the plan**:
approval or adjustments before implementing anything.
4. **Implement.** Make the agreed changes.
5. **Reply to every comment.** Add a reply to **all** comments explaining how it
was addressed (or the agreed outcome) — leave none unanswered.
6. **Resolve resolved threads.** Mark a review thread as resolved only when the
comment has actually been addressed.
was addressed, preferably citing the commit containing the change. If the
feedback was not addressed, explain why. Leave no comment unanswered.
6. **Resolve completed threads yourself.** After replying and completing any
necessary discussion, resolve the review thread. Do not wait for the reviewer
or a maintainer to resolve it. Leave a thread open only while it has an
unanswered question or active discussion.
### Useful commands
+37
View File
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft. All rights reserved.
/**
* Tests for review_command.js.
*
* Run with: node --test .github/tests/test_review_command.js
*/
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const isReviewCommand = require('../scripts/review_command.js');
describe('review command validation', () => {
it('accepts the exact review command', () => {
assert.equal(isReviewCommand('/review'), true);
});
it('accepts surrounding whitespace', () => {
assert.equal(isReviewCommand('/review\r\n'), true);
assert.equal(isReviewCommand(' \n/review\t'), true);
});
it('rejects commands with additional content', () => {
assert.equal(isReviewCommand('/reviewer'), false);
assert.equal(isReviewCommand('/review please'), false);
assert.equal(isReviewCommand('/review\nadditional text'), false);
assert.equal(isReviewCommand('/Review'), false);
});
it('rejects missing or non-string comment bodies', () => {
assert.equal(isReviewCommand(''), false);
assert.equal(isReviewCommand(null), false);
assert.equal(isReviewCommand(undefined), false);
});
});
+3 -3
View File
@@ -38,7 +38,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -51,7 +51,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4
uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -64,6 +64,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
with:
category: "/language:${{matrix.language}}"
+29 -5
View File
@@ -34,18 +34,42 @@ env:
MODEL_CONFIG_PATH: ${{ github.workspace }}/devflow/config.ci.yaml
jobs:
team_check:
command_check:
if: >-
github.event_name != 'issue_comment' ||
(
github.event.issue.pull_request &&
github.event.comment.body == '/review' &&
(
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'OWNER'
)
)
runs-on: ubuntu-latest
outputs:
should_review: ${{ steps.check.outputs.should_review }}
steps:
- name: Checkout review command validation
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: .github/scripts/review_command.js
fetch-depth: 1
persist-credentials: false
- name: Check review command
id: check
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const isReviewCommand = require('./.github/scripts/review_command.js');
const shouldReview = context.eventName !== 'issue_comment' ||
isReviewCommand(context.payload.comment?.body);
core.setOutput('should_review', shouldReview ? 'true' : 'false');
team_check:
needs: command_check
if: ${{ needs.command_check.outputs.should_review == 'true' }}
runs-on: ubuntu-latest
environment: github-app-auth
outputs:
is_team_member: ${{ steps.check.outputs.is_team_member }}
@@ -111,7 +135,7 @@ jobs:
- name: Check review requester team membership
id: check
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
MEMBERSHIP_USER: ${{ github.event_name == 'issue_comment' && github.event.comment.user.login || '' }}
TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }}
@@ -137,7 +161,7 @@ jobs:
- name: React to authorized review command
if: ${{ github.event_name == 'issue_comment' && steps.check.outputs.is_team_member == 'true' }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.github-auth.outputs.token }}
script: |
@@ -184,7 +208,7 @@ jobs:
path: devflow
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.13"
+37 -7
View File
@@ -37,6 +37,7 @@ jobs:
outputs:
dotnetChanges: ${{ steps.filter.outputs.dotnet }}
cosmosDbChanges: ${{ steps.filter.outputs.cosmosdb }}
azureStorageChanges: ${{ steps.filter.outputs.azurestorage }}
foundryHostingChanges: ${{ steps.filter.outputs.foundryHosting }}
coreChanged: ${{ steps.filter.outputs.core }}
steps:
@@ -49,6 +50,12 @@ jobs:
- 'dotnet/**'
cosmosdb:
- 'dotnet/src/Microsoft.Agents.AI.CosmosNoSql/**'
azurestorage:
- 'dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/**'
- 'dotnet/tests/Microsoft.Agents.AI.Hosting.AzureStorage.IntegrationTests/**'
- 'dotnet/tests/Microsoft.Agents.AI.Hosting.AzureStorage.UnitTests/**'
- 'dotnet/Directory.Packages.props'
- '.github/workflows/dotnet-build-and-test.yml'
# The Foundry hosted-agent IT is costly (builds a container, pushes to ACR,
# provisions live agents). Only run it when the project under test, its
# dependency chain, the test container, the test fixture, or their tooling
@@ -120,6 +127,7 @@ jobs:
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
global-json-file: ${{ github.workspace }}/dotnet/global.json
- name: Build dotnet solutions
shell: bash
run: |
@@ -184,6 +192,7 @@ jobs:
.
.github
dotnet
docs/specs
python
declarative-agents
@@ -200,6 +209,27 @@ jobs:
Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
echo "COSMOSDB_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV
- name: Start Azurite Blob service
if: ${{ runner.os == 'Linux' && (needs.paths-filter.outputs.azureStorageChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }}
shell: bash
run: |
docker run --detach --rm \
--name azurite \
--publish 10000:10000 \
mcr.microsoft.com/azure-storage/azurite:3.35.0@sha256:647c63a91102a9d8e8000aab803436e1fc85fbb285e7ce830a82ee5d6661cf37 \
azurite-blob --blobHost 0.0.0.0 --blobPort 10000 --skipApiVersionCheck
for attempt in {1..30}; do
if (echo > /dev/tcp/127.0.0.1/10000) > /dev/null 2>&1; then
echo "AZURITE_AVAILABLE=true" >> "$GITHUB_ENV"
exit 0
fi
sleep 1
done
docker logs azurite
exit 1
- name: Setup dotnet
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
@@ -269,7 +299,7 @@ jobs:
- name: Azure CLI Login
if: github.event_name != 'pull_request' && matrix.integration-tests
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -325,7 +355,7 @@ jobs:
- name: Upload coverage report artifact
if: matrix.targetFramework == env.COVERAGE_FRAMEWORK
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }} # Artifact name
path: ./TestResults/Reports # Directory containing files to upload
@@ -337,7 +367,7 @@ jobs:
- name: Upload integration test results
if: always() && github.event_name != 'pull_request' && matrix.integration-tests
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dotnet-test-results-${{ matrix.targetFramework }}-${{ matrix.os }}
path: IntegrationTestResults/**/*.junit
@@ -383,7 +413,7 @@ jobs:
run: dotnet build dotnet/tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj -c "$configuration" --warnaserror
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -463,14 +493,14 @@ jobs:
- name: Fail workflow if tests failed
id: check_tests_failed
if: contains(join(needs.*.result, ','), 'failure')
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: core.setFailed('Integration Tests Failed!')
- name: Fail workflow if tests cancelled
id: check_tests_cancelled
if: contains(join(needs.*.result, ','), 'cancelled')
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: core.setFailed('Integration Tests Cancelled!')
@@ -528,7 +558,7 @@ jobs:
key: dotnet-integration-report-history-${{ github.run_id }}
- name: Upload trend report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dotnet-integration-test-report
path: |
@@ -77,7 +77,7 @@ jobs:
done
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
+2 -2
View File
@@ -58,7 +58,7 @@ jobs:
- name: Azure CLI Login
if: github.event_name != 'pull_request'
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -126,7 +126,7 @@ jobs:
- name: Upload results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: verify-samples-results
path: |
@@ -27,11 +27,11 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.11"
@@ -50,7 +50,7 @@ jobs:
- name: Resolve and authorize checkout ref
id: resolve
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
+3 -3
View File
@@ -94,7 +94,7 @@ jobs:
- name: Check issue author team membership
if: ${{ github.event_name != 'workflow_dispatch' }}
id: check
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }}
ISSUE_NUMBER: ${{ steps.issue.outputs.issue_number }}
@@ -153,7 +153,7 @@ jobs:
path: devflow
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.13"
@@ -168,7 +168,7 @@ jobs:
run: uv sync --frozen
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
repository: ${{ github.repository }}
fallback-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.github-auth.outputs.token }}
script: |
+2 -2
View File
@@ -46,12 +46,12 @@ jobs:
repository: ${{ github.repository }}
fallback-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
with:
repo-token: ${{ steps.github-auth.outputs.token }}
- name: "PR: add breaking change label from title"
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.github-auth.outputs.token }}
script: |
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
fetch-depth: 1
persist-credentials: false
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
name: "Issue/PR: update title"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
+2 -2
View File
@@ -53,7 +53,7 @@ jobs:
- name: Check PR author team membership
id: check
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }}
PR_NUMBER: ${{ github.event.pull_request.number }}
@@ -107,7 +107,7 @@ jobs:
fallback-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
- name: Enforce open PR limit
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.github-auth.outputs.token }}
script: |
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
@@ -33,7 +33,7 @@ jobs:
# Checks the status of hyperlinks in all files
- name: Run linkspector
uses: umbrelladocs/action-linkspector@963b6264d7de32c904942a70b488d3407453049e # v1
uses: umbrelladocs/action-linkspector@963b6264d7de32c904942a70b488d3407453049e # v1.5.1
with:
reporter: local
filter_mode: nofilter
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Wait for required checks
if: github.event_name == 'pull_request'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
TIMEOUT_SECONDS: "3600"
INTERVAL_SECONDS: "30"
@@ -78,7 +78,7 @@ jobs:
- name: Upload dependency validation reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dependency-maintenance-results
path: |
@@ -88,7 +88,7 @@ jobs:
- name: Create issue for failed dependency bounds test
if: steps.validate_bounds_test.outcome != 'success'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -158,7 +158,7 @@ jobs:
- name: Create issues for failed dependency candidates
if: always()
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -321,7 +321,7 @@ jobs:
- name: Create or update dependency maintenance tracking issue
if: steps.commit_updates.outputs.has_changes == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
+13 -13
View File
@@ -102,7 +102,7 @@ jobs:
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-openai
path: ./python/pytest.xml
@@ -138,7 +138,7 @@ jobs:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -156,7 +156,7 @@ jobs:
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-azure-openai
path: ./python/pytest.xml
@@ -248,7 +248,7 @@ jobs:
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-misc
path: ./python/pytest.xml
@@ -308,7 +308,7 @@ jobs:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -325,7 +325,7 @@ jobs:
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-foundry
path: ./python/pytest.xml
@@ -358,7 +358,7 @@ jobs:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Azure CLI Login
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -375,7 +375,7 @@ jobs:
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-foundry-hosting
path: ./python/pytest.xml
@@ -427,7 +427,7 @@ jobs:
run: uv run --directory packages/azure-cosmos poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5 --junitxml=${{ github.workspace }}/python/pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-cosmos
path: ./python/pytest.xml
@@ -469,7 +469,7 @@ jobs:
--junitxml=pytest.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-github-copilot
path: ./python/pytest.xml
@@ -535,7 +535,7 @@ jobs:
key: integration-report-history-integration-${{ github.run_id }}
- name: Upload unified trend report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: integration-test-report
path: |
@@ -559,12 +559,12 @@ jobs:
steps:
- name: Fail workflow if tests failed
if: contains(join(needs.*.result, ','), 'failure')
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: core.setFailed('Integration Tests Failed!')
- name: Fail workflow if tests cancelled
if: contains(join(needs.*.result, ','), 'cancelled')
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: core.setFailed('Integration Tests Cancelled!')
+13 -13
View File
@@ -187,7 +187,7 @@ jobs:
title: OpenAI integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-openai
path: ./python/pytest.xml
@@ -224,7 +224,7 @@ jobs:
os: ${{ runner.os }}
- name: Azure CLI Login
if: github.event_name != 'pull_request'
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -257,7 +257,7 @@ jobs:
title: Azure OpenAI integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-azure-openai
path: ./python/pytest.xml
@@ -382,7 +382,7 @@ jobs:
title: Misc integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-misc
path: ./python/pytest.xml
@@ -422,7 +422,7 @@ jobs:
os: ${{ runner.os }}
- name: Azure CLI Login
if: github.event_name != 'pull_request'
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -449,7 +449,7 @@ jobs:
title: Test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-foundry
path: ./python/pytest.xml
@@ -483,7 +483,7 @@ jobs:
os: ${{ runner.os }}
- name: Azure CLI Login
if: github.event_name != 'pull_request'
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -510,7 +510,7 @@ jobs:
title: Foundry Hosting integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-foundry-hosting
path: ./python/pytest.xml
@@ -577,7 +577,7 @@ jobs:
title: Cosmos integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-cosmos
path: ./python/pytest.xml
@@ -632,7 +632,7 @@ jobs:
title: GitHub Copilot integration test results
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results-github-copilot
path: ./python/pytest.xml
@@ -695,7 +695,7 @@ jobs:
key: integration-report-history-merge-${{ github.run_id }}
- name: Upload unified trend report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: integration-test-report
path: |
@@ -720,13 +720,13 @@ jobs:
- name: Fail workflow if tests failed
id: check_tests_failed
if: contains(join(needs.*.result, ','), 'failure')
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: core.setFailed('Integration Tests Failed!')
- name: Fail workflow if tests cancelled
id: check_tests_cancelled
if: contains(join(needs.*.result, ','), 'cancelled')
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: core.setFailed('Integration Tests Cancelled!')
+82 -13
View File
@@ -33,28 +33,97 @@ jobs:
env:
# Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache
- name: Set environment variables
- name: Resolve the package to build
env:
TAG_NAME: ${{ github.event.release.tag_name }}
run: |
# Extract package name from tag (format: python-<package>-<version>)
TAG="${{ github.event.release.tag_name }}"
PACKAGE=$(echo "$TAG" | sed 's/^python-\([^-]*\)-.*$/\1/')
set -euo pipefail
# Validate package exists
if [[ ! -d "packages/$PACKAGE" ]]; then
echo "Error: Package '$PACKAGE' not found in packages/ directory"
echo "Available packages: $(ls packages/)"
TAG="$TAG_NAME"
# Release tags are either python-<version> for the whole workspace, or
# python-<package>-<version> for a single package. Package names may
# themselves contain hyphens (hosting-a2a, azure-ai-search), so the
# package part cannot be found by splitting on the first hyphen.
#
# Versions follow the lifecycle patterns in the python-package-management
# skill: X.Y.Z, X.Y.ZaYYMMDD, X.Y.ZbYYMMDD, X.Y.ZrcN, each optionally
# carrying a .N or .postN re-cut suffix.
VERSION_PATTERN='^[0-9]+\.[0-9]+\.[0-9]+([ab][0-9]+|rc[0-9]+)?(\.[0-9]+|\.post[0-9]+)?$'
REST="${TAG#python-}"
if [[ -z "$REST" ]]; then
echo "Error: tag '$TAG' has no version or package component"
exit 1
fi
echo "PACKAGE=$PACKAGE" >> $GITHUB_ENV
echo "Building package: $PACKAGE"
if [[ "$REST" =~ $VERSION_PATTERN ]]; then
# python-<version>: build every workspace package plus the root meta package.
PACKAGE="all"
echo "Resolved tag '$TAG' to the full workspace build"
else
# python-<package>-<version>: split off the trailing version component and
# require it to be a real version, so a malformed tag fails here rather
# than being mistaken for another kind of release.
CANDIDATE="${REST%-*}"
VERSION="${REST##*-}"
if [[ "$CANDIDATE" == "$REST" || -z "$CANDIDATE" ]]; then
echo "Error: tag '$TAG' is neither python-<version> nor python-<package>-<version>"
exit 1
fi
if [[ ! "$VERSION" =~ $VERSION_PATTERN ]]; then
echo "Error: tag '$TAG' does not end in a supported version"
echo "Derived version: '$VERSION'"
exit 1
fi
# Resolve the package part against the real package directories. Tags use
# hyphens even where the directory uses underscores
# (python-github-copilot -> github_copilot).
PACKAGE=""
for dir in packages/*/; do
name="${dir#packages/}"
name="${name%/}"
if [[ "$name" == "$CANDIDATE" || "${name//_/-}" == "$CANDIDATE" ]]; then
PACKAGE="$name"
break
fi
done
if [[ -z "$PACKAGE" ]]; then
echo "Error: tag '$TAG' does not map to a directory in packages/"
echo "Derived package name: '$CANDIDATE'"
echo "Available packages: $(ls packages/)"
exit 1
fi
echo "Resolved tag '$TAG' to package '$PACKAGE'"
fi
echo "PACKAGE=$PACKAGE" >> "$GITHUB_ENV"
- name: Check version
env:
TAG_NAME: ${{ github.event.release.tag_name }}
run: |
echo "Building and uploading Python package version: ${{ github.event.release.tag_name }}"
echo "Package directory: packages/${{ env.PACKAGE }}"
echo "Building and uploading Python release: $TAG_NAME"
if [[ "$PACKAGE" == "all" ]]; then
echo "Build scope: all workspace packages and the root meta package"
else
echo "Build scope: packages/$PACKAGE"
fi
- name: Build the package
run: uv run poe --directory packages/${{ env.PACKAGE }} build
run: |
set -euo pipefail
if [[ "$PACKAGE" == "all" ]]; then
uv run poe build
else
uv run poe --directory "packages/$PACKAGE" build
fi
- name: Release
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
with:
+20 -20
View File
@@ -53,7 +53,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-01-get-started
@@ -120,7 +120,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-02-agents
@@ -167,7 +167,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-02-agents-harness
@@ -209,7 +209,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-02-agents-tools
@@ -255,7 +255,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-02-agents-openai
@@ -299,7 +299,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-02-agents-azure
@@ -341,7 +341,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-02-agents-anthropic
@@ -375,7 +375,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-02-agents-github-copilot
@@ -411,7 +411,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-02-agents-amazon
@@ -447,7 +447,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-02-agents-ollama
@@ -493,7 +493,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-02-agents-foundry
@@ -539,7 +539,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-02-agents-copilotstudio
@@ -572,7 +572,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-02-agents-custom
@@ -614,7 +614,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-03-workflows
@@ -661,7 +661,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-04-hosting-foundry-hosted-agents
@@ -700,7 +700,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-04-hosting-other
@@ -747,7 +747,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-05-end-to-end
@@ -804,7 +804,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-autogen-migration
@@ -873,7 +873,7 @@ jobs:
uses: ./.github/actions/sample-validation-save-playbooks
- name: Upload validation report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-report-semantic-kernel-migration
@@ -939,7 +939,7 @@ jobs:
key: validation-history-${{ github.run_id }}
- name: Upload trend report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: validation-trend-report
@@ -22,7 +22,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download coverage report
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ github.token }}
run-id: ${{ github.event.workflow_run.id }}
+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
- name: Check coverage threshold
run: python ${{ github.workspace }}/.github/scripts/python_check_coverage.py python-coverage.xml ${{ env.COVERAGE_THRESHOLD }}
- name: Upload coverage report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
path: |
python/python-coverage.xml
+1 -1
View File
@@ -50,7 +50,7 @@ jobs:
repository: ${{ github.repository }}
fallback-token: ${{ secrets.GH_ACTIONS_PR_WRITE }}
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.13'
+20 -1
View File
@@ -127,9 +127,28 @@ We use and recommend the following workflow:
7. Create a PR against the repository's **main** branch.
- State in the description what issue or improvement your change is addressing.
- Verify that all the Continuous Integration checks are passing.
8. Wait for feedback or approval of your changes from the code maintainers.
8. Address feedback from the code maintainers. Reply to every review comment with
the outcome and resolve each completed review conversation yourself before
requesting another review.
9. When area owners have signed off, and all checks are green, your PR will be merged.
### Resolving PR Review Comments
PR authors are responsible for closing out all review conversations on their pull
requests, including conversations opened by reviewers. Do not wait for the reviewer
or a maintainer to resolve completed conversations for you.
For every review comment:
- If the feedback was addressed, reply with a brief explanation and, preferably,
the commit containing the change.
- If the feedback was not addressed, reply with the reason why.
After replying and completing any necessary discussion, **resolve the conversation
yourself**. Leave a conversation open only while it has an unanswered question or
active discussion. Reviewers may reopen a conversation if further changes or
discussion are needed.
### Development Setup
Each language has its own dev setup guide, coding standards, and build scripts:
+5 -1
View File
@@ -11,7 +11,10 @@
Microsoft Agent Framework (MAF) is an open, multi-language framework for building **production-grade AI agents and multi-agent workflows** in **.NET and Python**.
Microsoft Agent Framework is built for teams taking agents from prototype to production. It provides a consistent foundation for building, orchestrating, and operating agent systems across Python and .NET, while keeping architecture choices open as requirements evolve, and supports a broad ecosystem including Microsoft Foundry, Azure OpenAI, OpenAI, and the GitHub Copilot SDK, with samples and hosting patterns for both local development and cloud deployment.
Microsoft Agent Framework is built for teams taking agents from prototype to production. It provides a consistent foundation for building, orchestrating, and operating agent systems across Python, .NET and Go, while keeping architecture choices open as requirements evolve, and supports a broad ecosystem including Microsoft Foundry, Azure OpenAI, OpenAI, and the GitHub Copilot SDK, with samples and hosting patterns for both local development and cloud deployment.
> [!NOTE]
> For the Go SDK, including its documentation, samples, contribution guidance, and issue tracker, visit [microsoft/agent-framework-go](https://github.com/microsoft/agent-framework-go/).
<p align="center">
<a href="https://www.youtube.com/watch?v=AAgdMhftj8w" title="Watch the full Agent Framework introduction (30 min)">
@@ -39,6 +42,7 @@ Explore new MAF capabilities and real implementation patterns on the [official b
- **Python and C#/.NET Support**: Full framework support for both Python and C#/.NET implementations with consistent APIs
- [Python packages](./python/packages/) | [.NET source](./dotnet/src/)
- **Go Support**: For the Go SDK, including its documentation, samples, contribution guidance, and issue tracker, visit [microsoft/agent-framework-go](https://github.com/microsoft/agent-framework-go/).
- **Multiple Agent Provider Support**: Support for various LLM providers with more being added continuously
- [Python examples](./python/samples/02-agents/providers/) | [.NET examples](./dotnet/samples/02-agents/AgentProviders/)
- **Middleware**: Flexible middleware system for request/response processing, exception handling, and custom pipelines
@@ -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
@@ -0,0 +1,42 @@
---
status: proposed
contact: MohammadHaroonAbuomar
date: 2026-08-07
deciders: agent-framework .NET maintainers
---
# .NET agent-hooks enforcement: composed factory over three seams
## Context and Problem Statement
The [AGENT-HOOKS-0.1](https://github.com/responsibleai/agent-hooks) interception contract shipped for Python as a first-class experimental core feature (#7515): a middleware bundle emitting eight interception points with three-verdict, fail-closed enforcement, transform write-back, buffered streaming, and verdict-before-durability persistence gating. The .NET side needs the same semantics, but the .NET framework has no category-based middleware lists — interception is decorator composition (`DelegatingAIAgent`, Microsoft.Extensions.AI `DelegatingChatClient`, the function-invocation middleware seam). How should the contract's indivisibility and enforcement properties be realized in that model?
## Decision Drivers
- Identical enforcement semantics to the merged Python feature (same spec, same fail-closed rules), diverging only where the .NET seam model requires it — never by weakening an enforcement property.
- Partial installation of the enforcement must be impossible or loudly rejected, not silently degraded.
- Denied content must never become durable; transformed content must persist post-transform.
- No changes to existing framework source; the optional native-runtime dependency (`ResponsibleAI.AgentHooks`) must not be referenced by core packages.
## Decision Outcome
**A single factory (`AsAIAgentWithAgentHooks`, per-run and host-owned-session overloads) in a new package `Microsoft.Agents.AI.AgentHooks` composes the full enforcement itself** instead of exposing middleware values:
- **Seam order (fixed by construction):** `AgentHooksAgent` (agent seam: `agent_startup`/`input`/`output`/`agent_shutdown`, per-run `AsyncLocal` state, buffered streaming, persistence gate) → framework function-invocation middleware (`pre_tool_call`/`post_tool_call`) → `ChatClientAgent` with its default pipeline → `AgentHooksChatClient` **below** `FunctionInvokingChatClient` (so `pre_model_call`/`post_model_call` bracket every model service call of the tool loop individually).
- **Indivisibility:** the seam decorators are `internal`; only the factory composes them. Two pipeline-replacement affordances of `ChatClientAgent` are rejected loudly (fail closed): a caller-supplied per-run `ChatClientFactory` (the framework's own function-middleware factory is recognized and allowed — it wraps, not replaces), and a supplied chat client that already contains a `FunctionInvokingChatClient` (it would execute tools below the verdicts).
- **Verdict-before-durability:** end-of-run history and context-provider writes defer behind the `output` verdict via gating provider wrappers installed by the factory (dropped on deny, flushed post-transform with verdicted-message substitution for streamed runs). The implicit default `InMemoryChatHistoryProvider` is materialized and gated, with the history-conflict flags set to mimic implicit-default semantics. Per-service-call persistence sits above the chat seam, so it is covered by its own `post_model_call` verdict. Per-run provider overrides are wrapped in both `AdditionalProperties` dictionaries, copy-on-write. Nested agents persist inline at their own boundaries (they have their own providers) — no run-identity bookkeeping is needed, unlike Python.
- **Fail-closed error behavior:** interceptor crashes/timeouts surface as `host_error:*` denies; enforcement-layer failures at the tool seam halt the run through `FunctionInvocationContext.Terminate` (the loop's only loud escape — thrown exceptions are converted to tool errors by the loop, which would fail open); wire projections run inside the guarded blocks; failure notifications to providers are redacted (empty request messages) once a deny/halt stands.
- **Streaming:** fully buffered per the spec's `buffered_output` semantics — zero egress ahead of a verdict; transformed responses re-derive the released updates (preserving continuation tokens) so egress never diverges from verdicted content.
### Considered Alternatives
- **Port Python's middleware-value model (a `MiddlewareBundle` type):** rejected — .NET has no middleware list to put a bundle into; indivisibility via runtime validation is weaker than construction ownership.
- **Core-framework persistence gate (as Python added in `_sessions.py`):** rejected — unnecessary in .NET; construction ownership of the provider instances gives the same property with zero core changes.
- **Per-run `ChatClientFactory` as the chat-seam install point:** rejected — it wraps the whole pipeline above the function-invocation loop, so per-model-call points would be impossible.
## Consequences
- Good: zero existing-source changes; the optional native dependency is isolated in one leaf package; enforcement properties are structural rather than convention-based.
- Accepted: the package ships in the release solution filter as an **alpha** package (maintainer decision on the PR) — the version suffix follows the maturity of the `ResponsibleAI.AgentHooks` dependency it is built on, and the whole surface stays `[Experimental]`; a sample follows once the API shape settles.
- Known limitations (documented on the factory): hosted (service-executed) tools never reach the function seam and are intercepted via the `post_model_call` content projection; service-managed (conversation-id) history is durable at the service and ungateable; the deferred-OTel decorator sits above the chat seam, so sensitive-data request spans observe pre-transform content; a chat-seam projection failure fails the run closed but without a synthesized `host_error` record (SDK affordance gap, responsibleai/agent-hooks#70).
- The trust model is the spec's: cooperative contract, not a security boundary — the misuse rejections catch accidental foot-guns loudly, not in-process adversaries.
+26 -2
View File
@@ -18,7 +18,7 @@ It covers:
- approved, rejected, mixed, and replayed approval rounds;
- reasoning content and opaque reasoning signatures bound to function calls;
- history persistence and service-side continuation;
- error, user-input, middleware-termination, and loop-limit paths;
- error, user-input, middleware-termination, middleware-failure, and loop-limit paths;
- provider and transport serialization of function calls and results.
The primary implementation is in `python/packages/core/agent_framework/_tools.py`. History replay behavior in
@@ -319,7 +319,24 @@ that manually replay messages own the equivalent rule: do not resend an approval
### Function calls and results
- Every actionable local `function_call` produces exactly one terminal `function_result`, unless execution pauses
for a new user-input request.
for a new user-input request or the run is aborted by `MiddlewareFailure`.
- An ordinary exception raised by function middleware or a tool body becomes one terminal error `function_result`
and the loop continues; `MiddlewareFailure` is the loop's only fail-closed escape: it is never converted into a
tool result, the in-flight parallel batch is cancelled, no further tool call starts, no further model turn is
consumed, and the exception propagates to the caller (for streaming runs, when the stream is consumed). On a
service-managed conversation the loop first settles the aborted batch — one error `function_result` per dangling
call (approval-response wrappers unwrap to their underlying calls; hosted-tool approvals are left to their own
provider protocol), submitted with `tool_choice="none"` in a single extra request — so the hosted thread is not
left ending in unresolved function calls that the service would reject on the session's next request; the
persisted continuation then advances to the settlement response (for response-ID continuations the settled
endpoint is the new handle; for conversation-object ids the advance is a no-op) and the settlement response is
otherwise discarded. Settlement covers the approval-resolution phase too: a fatal abort while an approved tool is
replayed settles the original, already-persisted calls. Without a service-managed conversation no extra request
is made. Batch
cancellation is cooperative: an async sibling stops at its next suspension point, while a synchronous tool body
already executing in a worker thread cannot be interrupted and may complete its side effects — its result is
discarded either way and never reaches the transcript, the model, or history. Middleware must not catch
`MiddlewareFailure` — swallowing it converts a fail-closed abort back into a running, possibly unguarded loop.
- Parallel calls retain model order in the returned transcript.
- Reused `call_id` values are correlated by logical occurrence, not one global value per id.
- A completed function call/result pair is inert on later turns.
@@ -491,6 +508,7 @@ that manually replay messages own the equivalent rule: do not resend an approval
| 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 shared workflow interrupt ownership | A direct shared `Workflow` request-info interrupt can only be resolved or cancelled by the Snapshot Scope and AG-UI thread that created it. Ownership follows the authoritative pending request occurrence, and explicitly threaded cold checkpoint resumes fail closed when ownership is unavailable. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_workflow_request_info_rejects_resume_from_different_thread`, `test_endpoint_workflow_request_info_rejects_resume_from_different_scope`, `test_endpoint_workflow_request_info_rejects_cancellation_from_different_thread`, `test_endpoint_workflow_request_info_remains_owned_after_client_disconnect`, `test_endpoint_workflow_request_info_rejects_unowned_pending_interrupt`, `test_endpoint_workflow_checkpoint_resume_rejects_threaded_resume_after_restart` |
| 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` |
@@ -506,6 +524,10 @@ that manually replay messages own the equivalent rule: do not resend an approval
| Consecutive error cap | Error threshold stops repeated failures, submits collected results, and makes only the required final no-tool model call. | `test_function_invocation_config_max_consecutive_errors`, `test_streaming_function_invocation_config_max_consecutive_errors`, `test_approval_resume_error_limit_forces_final_no_tool_response` |
| Unknown call handling | Configured false returns an error result; configured true raises. | `test_function_invocation_config_terminate_on_unknown_calls_false`, `test_function_invocation_config_terminate_on_unknown_calls_true`, streaming equivalents |
| Middleware termination | Normal non-approval loop stops without a second model call. | `test_terminate_loop_single_function_call`, `test_terminate_loop_multiple_function_calls_one_terminates`, `test_terminate_loop_streaming_single_function_call` |
| Middleware failure (fatal) | `MiddlewareFailure` from function middleware or a tool body propagates to the caller without becoming a tool result; the tool does not execute (pre-invocation) or its result never feeds another model call (post-invocation); the cause chain is preserved; ordinary exceptions still become tool-error results and the loop continues. | `packages/core/tests/core/test_middleware_with_agent.py::TestMiddlewareFailure::test_failure_before_tool_aborts_run`, `test_failure_after_tool_aborts_run_before_next_model_turn`, `test_failure_cause_chain_reaches_caller`, `test_failure_from_tool_escapes_without_middleware`, `test_failure_streaming_reaches_stream_consumer`, `test_ordinary_exception_still_becomes_tool_error` |
| Middleware failure batch cancellation | A fatal signal fails the whole parallel batch: in-flight sibling tool invocations are cancelled and awaited before the failure propagates. Cancellation is cooperative — an async sibling stops at its next suspension point; a synchronous tool body already executing in a worker thread cannot be interrupted and may complete its side effects, but its result is discarded and never reaches the transcript, the model, or history, and failure propagation is not delayed behind it. | `TestMiddlewareFailure::test_failure_cancels_concurrent_sibling_tool`, `test_failure_with_sync_sibling_discards_late_result` |
| Middleware failure on a service-managed conversation | The continuation state is already persisted when the batch fails, so before propagating, the loop settles the hosted thread: one error `function_result` per dangling call, sent with `tool_choice="none"` in one extra request; the persisted continuation advances to the settlement response (required for response-ID continuations, a no-op for conversation-object ids) and the settlement response is otherwise discarded; a settlement failure never masks the abort. Without a service-managed conversation no extra request is made. | `TestMiddlewareFailure::test_failure_settles_dangling_calls_on_service_conversation`, `test_failure_settles_service_conversation_streaming`, `test_failure_settlement_advances_response_id_continuation`, `test_failure_without_service_conversation_makes_no_settlement_request` |
| Middleware failure during approved-tool replay | A fatal abort while the approval-resolution phase replays an approved tool escapes loudly (never absorbed into a rejection result), the tool's original — already service-persisted — call is settled the same way, and the continuation advances; both response modes. | `TestMiddlewareFailure::test_failure_during_approved_replay_settles_and_escapes`, `test_failure_during_approved_replay_streaming` |
| Maximum iterations | No orphan calls; a final no-tool response or deterministic fallback is returned. | `test_max_iterations_limit`, `test_max_iterations_no_orphaned_function_calls`, `test_max_iterations_makes_final_toolchoice_none_call`, `test_max_iterations_blank_final_fallback_synthesizes_message`, streaming equivalents |
| Maximum function calls | Parallel overshoot is bounded after the batch; every executed result group counts even without a `function_result`; blank final responses get fallback content. | `test_max_function_calls_limits_parallel_invocations`, `test_max_function_calls_single_calls_per_iteration`, `test_user_input_request_multiple_contents_propagate`, `test_approval_resume_user_input_counts_toward_function_call_budget`, `test_max_function_calls_blank_final_fallback_synthesizes_message`, streaming equivalent |
| Provider tool content after an active limit | Locally actionable calls and local approval requests returned despite `tool_choice="none"` are removed in both response modes. Provider-executed informational call/result pairs, hosted approval requests, and metadata-only streaming updates remain visible; fallback text never replaces retained transcript content. | `test_function_invocation_limit_drops_unexecutable_tool_content`, `test_streaming_function_invocation_limit_drops_unexecutable_tool_content`, `test_streaming_function_invocation_limit_preserves_metadata_after_tool_content_is_dropped`, `test_function_invocation_limit_preserves_provider_executed_tool_pair`, `test_streaming_function_invocation_limit_preserves_provider_executed_tool_pair`, `test_function_invocation_limit_appends_fallback_after_provider_executed_tool_pair`, `test_streaming_function_invocation_limit_appends_fallback_after_provider_executed_tool_pair`, `test_function_invocation_limit_preserves_hosted_approval_request`, `test_streaming_function_invocation_limit_preserves_hosted_approval_request` |
@@ -534,6 +556,7 @@ that manually replay messages own the equivalent rule: do not resend an approval
| 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 client-tool request isolation | Client tool declarations are validated before use and remain request-scoped; a rejected collision or earlier successful request cannot change a later request's server-tool execution. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_failed_client_tool_collision_does_not_affect_next_request`, `test_endpoint_client_tools_do_not_persist_into_next_request` |
| 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` |
@@ -593,6 +616,7 @@ Before accepting an update, reviewers must confirm:
## Related issues
- #7241 — approval-resolution result streaming
- #7522 — first-class fatal signal (`MiddlewareFailure`) for function middleware
- #7267 / #7271 and #7304 — replayed calls and reused ids
- #7043 — provider-injected approval execution
- #6828 — AG-UI `confirm_changes` snapshot correlation
+6 -3
View File
@@ -74,9 +74,12 @@ code before the user has reviewed the plan**:
approval or adjustments before implementing anything.
4. **Implement.** Make the agreed changes.
5. **Reply to every comment.** Add a reply to **all** comments explaining how it
was addressed (or the agreed outcome) — leave none unanswered.
6. **Resolve resolved threads.** Mark a review thread as resolved only when the
comment has actually been addressed.
was addressed, preferably citing the commit containing the change. If the
feedback was not addressed, explain why. Leave no comment unanswered.
6. **Resolve completed threads yourself.** After replying and completing any
necessary discussion, resolve the review thread. Do not wait for the reviewer
or a maintainer to resolve it. Leave a thread open only while it has an
unanswered question or active discussion.
### Useful commands
+29 -26
View File
@@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="12.35.1" />
<PackageVersion Include="Anthropic" Version="12.42.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.7.1" />
<PackageVersion Include="Aspire.Hosting" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
@@ -23,15 +23,16 @@
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
<PackageVersion Include="MessagePack" Version="3.1.7" /> <!-- Transitive dependency of Aspire pinned to newer version due to vulnerability in 2.5.192 -->
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.26" />
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.5" />
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.6" />
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.28" />
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.6" />
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.8" />
<PackageVersion Include="Azure.Search.Documents" Version="12.0.0" />
<PackageVersion Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
<PackageVersion Include="Azure.Core" Version="1.60.0" />
<PackageVersion Include="Azure.Core" Version="1.61.0" />
<PackageVersion Include="Azure.Identity" Version="1.21.0" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.29.1" />
<PackageVersion Include="DotNetEnv" Version="3.1.1" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.5.0" />
<!-- Google Gemini -->
@@ -42,25 +43,25 @@
<!-- Newtonsoft.Json -->
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<!-- System.* -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.9" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.11" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.5" />
<PackageVersion Include="System.ClientModel" Version="1.14.0" />
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.10" />
<PackageVersion Include="System.ClientModel" Version="1.15.0" />
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.10" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.9" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.11" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.5" />
<PackageVersion Include="System.Net.Http.Json" Version="10.0.0" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.8" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.10" />
<!-- AG-UI .NET SDK packages (published by the AG-UI team). -->
<PackageVersion Include="AGUI.Abstractions" Version="0.0.3" />
<PackageVersion Include="AGUI.Formatting" Version="0.0.3" />
<PackageVersion Include="AGUI.Protobuf" Version="0.0.3" />
<PackageVersion Include="AGUI.Client" Version="0.0.3" />
<PackageVersion Include="AGUI.Server" Version="0.0.3" />
<PackageVersion Include="System.Text.Json" Version="10.0.9" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.9" />
<PackageVersion Include="AGUI.Abstractions" Version="0.0.5" />
<PackageVersion Include="AGUI.Formatting" Version="0.0.5" />
<PackageVersion Include="AGUI.Protobuf" Version="0.0.5" />
<PackageVersion Include="AGUI.Client" Version="0.0.5" />
<PackageVersion Include="AGUI.Server" Version="0.0.5" />
<PackageVersion Include="System.Text.Json" Version="10.0.11" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.11" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
<!-- OpenTelemetry -->
@@ -80,11 +81,11 @@
<PackageVersion Include="Microsoft.OpenApi" Version="2.7.5" /> <!-- Pin patched OpenAPI.NET to remediate GHSA-v5pm-xwqc-g5wc -->
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
<!-- Microsoft.Extensions.* -->
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.7.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.7.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation" Version="10.7.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Quality" Version="10.7.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Safety" Version="10.7.0" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.9.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.9.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation" Version="10.9.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Quality" Version="10.9.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Safety" Version="10.9.0" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.6.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Compliance.Abstractions" Version="10.5.0" />
@@ -94,12 +95,12 @@
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="10.7.0" />
@@ -109,6 +110,7 @@
<PackageVersion Include="CommunityToolkit.VectorData.Qdrant" Version="1.0.0" />
<!-- Agent SDKs -->
<PackageVersion Include="GitHub.Copilot.SDK" Version="1.0.5" />
<PackageVersion Include="ResponsibleAI.AgentHooks" Version="0.1.0-alpha.4" />
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.171-beta" />
<!-- M365 Agents SDK -->
<PackageVersion Include="AdaptiveCards" Version="3.1.0" />
@@ -118,7 +120,8 @@
<PackageVersion Include="A2A" Version="1.0.0-preview2" />
<PackageVersion Include="A2A.AspNetCore" Version="1.0.0-preview2" />
<!-- MCP -->
<PackageVersion Include="ModelContextProtocol" Version="1.2.0" />
<PackageVersion Include="ModelContextProtocol" Version="2.1.0" />
<PackageVersion Include="ModelContextProtocol.Extensions.Tasks" Version="2.1.0" />
<!-- Hyperlight -->
<PackageVersion Include="Hyperlight.HyperlightSandbox.Api" Version="0.4.0" />
<PackageVersion Include="Hyperlight.HyperlightSandbox.Guest.Python" Version="0.4.0" />
-1
View File
@@ -33,4 +33,3 @@ Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Fram
- [Design Documents](../docs/design)
- [Architectural Decision Records](../docs/decisions)
- [MSFT Learn Docs](https://learn.microsoft.com/agent-framework/overview/agent-framework-overview)
+13 -1
View File
@@ -68,6 +68,7 @@
<Project Path="samples/02-agents/Agents/Agent_Step21_ShellWithEnvironment/Agent_Step21_ShellWithEnvironment.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step22_AgentMode/Agent_Step22_AgentMode.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step23_TodoList/Agent_Step23_TodoList.csproj" />
<Project Path="samples/02-agents/Agents/Agent_Step24_MultiModelRouting/Agent_Step24_MultiModelRouting.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/DeclarativeAgents/">
<Project Path="samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
@@ -109,6 +110,10 @@
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step01_MeetYourClaw/Claw_Step01_MeetYourClaw.csproj" />
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step02_WorkingWithData/Claw_Step02_WorkingWithData.csproj" />
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step03_ScalingCapabilities/Claw_Step03_ScalingCapabilities.csproj" />
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step04_ProductionReady/ClawAgent/ClawAgent.csproj" />
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step04_ProductionReady/ClawAgent.Console/ClawAgent.Console.csproj" />
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step04_ProductionReady/ClawAgent.Hosted/ClawAgent.Hosted.csproj" />
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step04_ProductionReady/ClawAgent.Evals/ClawAgent.Evals.csproj" />
<Project Path="samples/02-agents/Harness/ConsoleReactiveComponents/ConsoleReactiveComponents.csproj" />
<Project Path="samples/02-agents/Harness/ConsoleReactiveFramework/ConsoleReactiveFramework.csproj" />
<Project Path="samples/02-agents/Harness/Harness_Shared_Console/Harness_Shared_Console.csproj" />
@@ -600,11 +605,13 @@
<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.AzureStorage/Microsoft.Agents.AI.Hosting.AzureStorage.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" />
<Project Path="src/Microsoft.Agents.AI.LocalCodeAct/Microsoft.Agents.AI.LocalCodeAct.csproj" />
<Project Path="src/Microsoft.Agents.AI.Mcp/Microsoft.Agents.AI.Mcp.csproj" />
<Project Path="src/Microsoft.Agents.AI.AgentHooks/Microsoft.Agents.AI.AgentHooks.csproj" />
<Project Path="src/Microsoft.Agents.AI.Mem0/Microsoft.Agents.AI.Mem0.csproj" />
<Project Path="src/Microsoft.Agents.AI.OpenAI/Microsoft.Agents.AI.OpenAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Purview/Microsoft.Agents.AI.Purview.csproj" />
@@ -630,6 +637,7 @@
<Project Path="tests/Foundry.IntegrationTests/Foundry.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.AzureStorage.IntegrationTests/Microsoft.Agents.AI.Hosting.AzureStorage.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" />
@@ -642,17 +650,22 @@
<Project Path="tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/Aspire.Hosting.AgentFramework.DevUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AgentHooks.UnitTests/Microsoft.Agents.AI.AgentHooks.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.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.FeatureRegistry.UnitTests/Microsoft.Agents.AI.FeatureRegistry.UnitTests.csproj">
<Build Solution="Debug|*" Project="false" />
</Project>
<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.AzureStorage.UnitTests/Microsoft.Agents.AI.Hosting.AzureStorage.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" />
@@ -670,4 +683,3 @@
<Project Path="tests/Microsoft.Agents.AI.Workflows.UnitTests/Microsoft.Agents.AI.Workflows.UnitTests.csproj" />
</Folder>
</Solution>
+2
View File
@@ -4,6 +4,7 @@
"projects": [
"src\\Microsoft.Agents.AI.A2A\\Microsoft.Agents.AI.A2A.csproj",
"src\\Microsoft.Agents.AI.Abstractions\\Microsoft.Agents.AI.Abstractions.csproj",
"src\\Microsoft.Agents.AI.AgentHooks\\Microsoft.Agents.AI.AgentHooks.csproj",
"src\\Microsoft.Agents.AI.Anthropic\\Microsoft.Agents.AI.Anthropic.csproj",
"src\\Microsoft.Agents.AI.GitHub.Copilot\\Microsoft.Agents.AI.GitHub.Copilot.csproj",
"src\\Microsoft.Agents.AI.Harness\\Microsoft.Agents.AI.Harness.csproj",
@@ -19,6 +20,7 @@
"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.AzureStorage\\Microsoft.Agents.AI.Hosting.AzureStorage.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
@@ -32,4 +32,7 @@
<ItemGroup Condition="'$(InjectSharedRedaction)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\Redaction\*.cs" LinkBase="Shared\Redaction" />
</ItemGroup>
<ItemGroup Condition="'$(InjectSharedFeatureUsageUserAgent)' == 'true'">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\FeatureUsage\*.cs" LinkBase="Shared\FeatureUsage" />
</ItemGroup>
</Project>
@@ -1303,6 +1303,7 @@ internal static class AgentsSamples
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
MustContain =
[
"MCP 2026-07-28 Tasks extension enabled.",
"=== Transparent long-running MCP task (RunAsync) ===",
"=== Transparent long-running MCP task (RunStreamingAsync) ===",
],
+1 -1
View File
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.302",
"version": "10.0.303",
"rollForward": "minor",
"allowPrerelease": false
},
+3 -3
View File
@@ -1,14 +1,14 @@
<Project>
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.17.0</VersionPrefix>
<VersionPrefix>1.18.0</VersionPrefix>
<RCNumber>1</RCNumber>
<DateSuffix>260804</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.17.0</GitTag>
<GitTag>1.18.0</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
+14 -18
View File
@@ -51,7 +51,7 @@ dotnet run --urls http://localhost:8888
An interactive console client that connects to an AG-UI server. Demonstrates:
- Creating an AG-UI client with `AGUIChatClient`
- Managing conversation threads
- Managing multi-turn conversations with an `AgentSession`
- Streaming responses with `RunStreamingAsync`
- Displaying colored console output for different content types
- Supporting both interactive and automated modes
@@ -133,28 +133,24 @@ Demonstrates human-in-the-loop approval workflows for sensitive operations. This
An AG-UI server that implements approval workflows. Demonstrates:
- Wrapping tools with `ApprovalRequiredAIFunction`
- Converting `FunctionApprovalRequestContent` to approval requests
- Middleware pattern with `ServerFunctionApprovalServerAgent`
- Complete function call capture and restoration
- Wrapping a tool with `ApprovalRequiredAIFunction` so it requires approval before running
- Mapping a plain agent with `MapAGUIServer`, which natively emits an approval interrupt when the model calls the approval-required tool and resumes the run once the client sends the decision back
**Run the server:**
```bash
cd Step04_HumanInLoop/Server
dotnet run --urls http://localhost:8888
dotnet run --urls http://localhost:5100
```
#### Client (`Step04_HumanInLoop/Client`)
An interactive client that handles approval requests from the server. Demonstrates:
- Using `ServerFunctionApprovalClientAgent` middleware
- Detecting `FunctionApprovalRequestContent`
- Displaying approval details to users
- Prompting for approval/rejection
- Sending approval responses with `FunctionApprovalResponseContent`
- Resuming conversation after approval
- Detecting `ToolApprovalRequestContent` in the streamed response
- Displaying approval details to the user and prompting for approval or rejection
- Sending the decision back as a `ToolApprovalResponseContent` created with `approvalRequest.CreateResponse(approved)`
- Resuming the run so the server continues after the decision is received
**Run the client:**
@@ -167,15 +163,15 @@ Try asking the agent to perform sensitive operations like "Approve expense repor
### Step05_StateManagement
An AG-UI server and client that demonstrate state management with predictive updates.
An AG-UI server and client that demonstrate shared state management.
#### Server (`Step05_StateManagement/Server`)
Demonstrates:
- Defining state schemas using C# records
- Using `SharedStateAgent` middleware for state management
- Streaming predictive state updates with `AgentState` content
- Exposing a `generate_recipe` tool that returns the complete recipe
- Mapping the tool result to a `STATE_SNAPSHOT` event with `AGUIStreamOptions.MapResultAsStateSnapshot`
- Reading the client's current recipe from `RunAgentInput.State`
- Managing shared state between client and server
- Using JSON serialization contexts for state types
@@ -210,7 +206,7 @@ dotnet run
### Client-Side
1. `AGUIAgent` sends HTTP POST request to server
1. `AGUIChatClient` sends HTTP POST request to server
2. Server responds with SSE stream
3. Client parses events into `AgentResponseUpdate` objects
4. Updates are displayed based on content type
@@ -228,7 +224,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 `UseClaimsBasedAgentIsolation(...)` 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 `MapAGUIServer` 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
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
using AGUI.Abstractions;
using AGUI.Client;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
@@ -50,7 +49,6 @@ try
// Stream the response
bool isFirstUpdate = true;
string? threadId = null;
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
{
@@ -59,11 +57,8 @@ try
// First update indicates run started
if (isFirstUpdate)
{
// AGUIChatClient is stateless and never surfaces a ConversationId; the thread
// id is carried on the AG-UI RUN_STARTED event's raw representation.
threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
Console.WriteLine($"\n[Run Started - Run: {chatUpdate.ResponseId}]");
Console.ResetColor();
isFirstUpdate = false;
}
@@ -86,8 +81,11 @@ try
}
}
// The session owns prior history, so the next run sends only the new user message.
messages.Clear();
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
Console.WriteLine("\n[Run Finished]");
Console.ResetColor();
}
}
@@ -7,7 +7,6 @@ using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using OpenAI.Chat;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
builder.Services.AddAGUIServer();
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
using AGUI.Abstractions;
using AGUI.Client;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
@@ -50,7 +49,6 @@ try
// Stream the response
bool isFirstUpdate = true;
string? threadId = null;
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
{
@@ -59,11 +57,8 @@ try
// First update indicates run started
if (isFirstUpdate)
{
// AGUIChatClient is stateless and never surfaces a ConversationId; the thread
// id is carried on the AG-UI RUN_STARTED event's raw representation.
threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
Console.WriteLine($"\n[Run Started - Run: {chatUpdate.ResponseId}]");
Console.ResetColor();
isFirstUpdate = false;
}
@@ -118,8 +113,11 @@ try
}
}
// The session owns prior history, so the next run sends only the new user message.
messages.Clear();
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
Console.WriteLine("\n[Run Finished]");
Console.ResetColor();
}
}
@@ -11,7 +11,6 @@ using Microsoft.Extensions.Options;
using OpenAI.Chat;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.TypeInfoResolverChain.Add(SampleJsonSerializerContext.Default));
builder.Services.AddAGUIServer();
@@ -75,6 +74,7 @@ AITool[] tools =
[
AIFunctionFactory.Create(
SearchRestaurants,
name: "search_restaurants",
serializerOptions: jsonOptions.SerializerOptions)
];
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using AGUI.Abstractions;
using AGUI.Client;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
@@ -19,7 +18,7 @@ static string GetUserLocation()
}
// Create frontend tools
AITool[] frontendTools = [AIFunctionFactory.Create(GetUserLocation)];
AITool[] frontendTools = [AIFunctionFactory.Create(GetUserLocation, name: "get_user_location")];
// Create the AG-UI client agent with tools
using HttpClient httpClient = new()
@@ -63,7 +62,6 @@ try
// Stream the response
bool isFirstUpdate = true;
string? threadId = null;
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
{
@@ -72,11 +70,8 @@ try
// First update indicates run started
if (isFirstUpdate)
{
// AGUIChatClient is stateless and never surfaces a ConversationId; the thread
// id is carried on the AG-UI RUN_STARTED event's raw representation.
threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
Console.WriteLine($"\n[Run Started - Run: {chatUpdate.ResponseId}]");
Console.ResetColor();
isFirstUpdate = false;
}
@@ -111,8 +106,11 @@ try
}
}
// The session owns prior history, so the next run sends only the new user message.
messages.Clear();
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
Console.WriteLine("\n[Run Finished]");
Console.ResetColor();
}
}
@@ -7,7 +7,6 @@ using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using OpenAI.Chat;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
builder.Services.AddAGUIServer();
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
@@ -15,17 +15,12 @@ using HttpClient httpClient = new()
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
// Create agent
ChatClientAgent baseAgent = chatClient.AsAIAgent(
// Create agent. No custom approval agent is required: the loop below handles the approval interrupt
// directly, and AGUIChatClient transports the decision back to the server via the AG-UI resume mechanism.
AIAgent agent = chatClient.AsAIAgent(
name: "AGUIAssistant",
instructions: "You are a helpful assistant.");
// Use default JSON serializer options
JsonSerializerOptions jsonSerializerOptions = JsonSerializerOptions.Default;
// Wrap the agent with ServerFunctionApprovalClientAgent
ServerFunctionApprovalClientAgent agent = new(baseAgent, jsonSerializerOptions);
List<ChatMessage> messages = [];
AgentSession? session = null;
@@ -44,7 +39,6 @@ while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringCompa
messages.Add(new ChatMessage(ChatRole.User, input));
Console.WriteLine();
#pragma warning disable MEAI001
List<AIContent> approvalResponses = [];
do
@@ -68,15 +62,6 @@ while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringCompa
ToolApprovalResponseContent approvalResponse = approvalRequest.CreateResponse(approved);
if (approvalRequest.AdditionalProperties != null)
{
approvalResponse.AdditionalProperties = [];
foreach (var kvp in approvalRequest.AdditionalProperties)
{
approvalResponse.AdditionalProperties[kvp.Key] = kvp.Value;
}
}
approvalResponses.Add(approvalResponse);
break;
@@ -115,11 +100,10 @@ while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringCompa
messages.AddRange(response.Messages);
foreach (AIContent approvalResponse in approvalResponses)
{
messages.Add(new ChatMessage(ChatRole.Tool, [approvalResponse]));
messages.Add(new ChatMessage(ChatRole.User, [approvalResponse]));
}
}
while (approvalResponses.Count > 0);
#pragma warning restore MEAI001
Console.WriteLine("\n");
Console.ForegroundColor = ConsoleColor.White;
@@ -127,7 +111,6 @@ while ((input = Console.ReadLine()) != null && !input.Equals("exit", StringCompa
Console.ResetColor();
}
#pragma warning disable MEAI001
static void DisplayApprovalRequest(ToolApprovalRequestContent approvalRequest, FunctionCallContent fcc)
{
Console.ForegroundColor = ConsoleColor.Yellow;
@@ -149,4 +132,3 @@ static void DisplayApprovalRequest(ToolApprovalRequestContent approvalRequest, F
Console.WriteLine("============================================================");
Console.ResetColor();
}
#pragma warning restore MEAI001
@@ -1,265 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using ServerFunctionApproval;
/// <summary>
/// A delegating agent that handles server function approval requests and responses.
/// Transforms between ToolApprovalRequestContent/ToolApprovalResponseContent
/// and the server's request_approval tool call pattern.
/// </summary>
internal sealed class ServerFunctionApprovalClientAgent : DelegatingAIAgent
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
public ServerFunctionApprovalClientAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
: base(innerAgent)
{
this._jsonSerializerOptions = jsonSerializerOptions;
}
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
.ToAgentResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Process and transform approval messages, creating a new message list
var processedMessages = ProcessOutgoingServerFunctionApprovals(messages.ToList(), this._jsonSerializerOptions);
// Run the inner agent and intercept any approval requests
await foreach (var update in this.InnerAgent.RunStreamingAsync(
processedMessages, session, options, cancellationToken).ConfigureAwait(false))
{
yield return ProcessIncomingServerApprovalRequests(update, this._jsonSerializerOptions);
}
}
#pragma warning disable MEAI001 // Type is for evaluation purposes only
private static FunctionResultContent ConvertApprovalResponseToToolResult(ToolApprovalResponseContent approvalResponse, JsonSerializerOptions jsonOptions)
{
return new FunctionResultContent(
callId: approvalResponse.RequestId,
result: JsonSerializer.SerializeToElement(
new ApprovalResponse
{
ApprovalId = approvalResponse.RequestId,
Approved = approvalResponse.Approved
},
jsonOptions));
}
private static List<ChatMessage> CopyMessagesUpToIndex(List<ChatMessage> messages, int index)
{
var result = new List<ChatMessage>(index);
for (int i = 0; i < index; i++)
{
result.Add(messages[i]);
}
return result;
}
private static List<AIContent> CopyContentsUpToIndex(IList<AIContent> contents, int index)
{
var result = new List<AIContent>(index);
for (int i = 0; i < index; i++)
{
result.Add(contents[i]);
}
return result;
}
private static List<ChatMessage> ProcessOutgoingServerFunctionApprovals(
List<ChatMessage> messages,
JsonSerializerOptions jsonSerializerOptions)
{
List<ChatMessage>? result = null;
Dictionary<string, ToolApprovalRequestContent> approvalRequests = [];
for (var messageIndex = 0; messageIndex < messages.Count; messageIndex++)
{
var message = messages[messageIndex];
List<AIContent>? transformedContents = null;
// Process each content item in the message
HashSet<string> approvalCalls = [];
for (var contentIndex = 0; contentIndex < message.Contents.Count; contentIndex++)
{
var content = message.Contents[contentIndex];
// Handle pending approval requests (transform to tool call)
if (content is ToolApprovalRequestContent approvalRequest &&
approvalRequest.AdditionalProperties?.TryGetValue("original_function", out var originalFunction) == true &&
originalFunction is FunctionCallContent original)
{
approvalRequests[approvalRequest.RequestId] = approvalRequest;
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
transformedContents.Add(original);
}
// Handle pending approval responses (transform to tool result)
else if (content is ToolApprovalResponseContent approvalResponse &&
approvalRequests.TryGetValue(approvalResponse.RequestId, out var correspondingRequest))
{
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
transformedContents.Add(ConvertApprovalResponseToToolResult(approvalResponse, jsonSerializerOptions));
approvalRequests.Remove(approvalResponse.RequestId);
correspondingRequest.AdditionalProperties?.Remove("original_function");
}
// Skip historical approval content
else if (content is FunctionCallContent { Name: "request_approval" } approvalCall)
{
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
approvalCalls.Add(approvalCall.CallId);
}
else if (content is FunctionResultContent functionResult &&
approvalCalls.Contains(functionResult.CallId))
{
transformedContents ??= CopyContentsUpToIndex(message.Contents, contentIndex);
approvalCalls.Remove(functionResult.CallId);
}
else
{
transformedContents?.Add(content);
}
}
if (transformedContents?.Count == 0)
{
continue;
}
else if (transformedContents != null)
{
// We made changes to contents, so use transformedContents
var newMessage = new ChatMessage(message.Role, transformedContents)
{
AuthorName = message.AuthorName,
MessageId = message.MessageId,
CreatedAt = message.CreatedAt,
RawRepresentation = message.RawRepresentation,
AdditionalProperties = message.AdditionalProperties
};
result ??= CopyMessagesUpToIndex(messages, messageIndex);
result.Add(newMessage);
}
else
{
// We're already copying messages, so copy this unchanged message too
result?.Add(message);
}
// If result is null, we haven't made any changes yet, so keep processing
}
return result ?? messages;
}
private static AgentResponseUpdate ProcessIncomingServerApprovalRequests(
AgentResponseUpdate update,
JsonSerializerOptions jsonSerializerOptions)
{
IList<AIContent>? updatedContents = null;
for (var i = 0; i < update.Contents.Count; i++)
{
var content = update.Contents[i];
if (content is FunctionCallContent { Name: "request_approval" } request)
{
updatedContents ??= [.. update.Contents];
// Serialize the function arguments as JsonElement
ApprovalRequest? approvalRequest;
if (request.Arguments?.TryGetValue("request", out var reqObj) == true &&
reqObj is JsonElement je)
{
approvalRequest = (ApprovalRequest?)je.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest)));
}
else
{
approvalRequest = null;
}
if (approvalRequest == null)
{
throw new InvalidOperationException("Failed to deserialize approval request.");
}
var functionCallArgs = (Dictionary<string, object?>?)approvalRequest.FunctionArguments?
.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(Dictionary<string, object?>)));
var approvalRequestContent = new ToolApprovalRequestContent(
requestId: approvalRequest.ApprovalId,
new FunctionCallContent(
callId: approvalRequest.ApprovalId,
name: approvalRequest.FunctionName,
arguments: functionCallArgs));
approvalRequestContent.AdditionalProperties ??= [];
approvalRequestContent.AdditionalProperties["original_function"] = content;
updatedContents[i] = approvalRequestContent;
}
}
if (updatedContents is not null)
{
var chatUpdate = update.AsChatResponseUpdate();
return new AgentResponseUpdate(new ChatResponseUpdate()
{
Role = chatUpdate.Role,
Contents = updatedContents,
MessageId = chatUpdate.MessageId,
AuthorName = chatUpdate.AuthorName,
CreatedAt = chatUpdate.CreatedAt,
RawRepresentation = chatUpdate.RawRepresentation,
ResponseId = chatUpdate.ResponseId,
AdditionalProperties = chatUpdate.AdditionalProperties
})
{
AgentId = update.AgentId,
ContinuationToken = update.ContinuationToken,
};
}
return update;
}
}
#pragma warning restore MEAI001
namespace ServerFunctionApproval
{
public sealed class ApprovalRequest
{
[JsonPropertyName("approval_id")]
public required string ApprovalId { get; init; }
[JsonPropertyName("function_name")]
public required string FunctionName { get; init; }
[JsonPropertyName("function_arguments")]
public JsonElement? FunctionArguments { get; init; }
[JsonPropertyName("message")]
public string? Message { get; init; }
}
public sealed class ApprovalResponse
{
[JsonPropertyName("approval_id")]
public required string ApprovalId { get; init; }
[JsonPropertyName("approved")]
public required bool Approved { get; init; }
}
}
@@ -5,26 +5,10 @@ using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.HttpLogging;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Options;
using OpenAI.Chat;
using ServerFunctionApproval;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpLogging(logging =>
{
logging.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders | HttpLoggingFields.RequestBody
| HttpLoggingFields.ResponsePropertiesAndHeaders | HttpLoggingFields.ResponseBody;
logging.RequestBodyLogLimit = int.MaxValue;
logging.ResponseBodyLogLimit = int.MaxValue;
});
builder.Services.AddHttpClient().AddLogging();
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.TypeInfoResolverChain.Add(ApprovalJsonContext.Default));
builder.Services.AddAGUIServer();
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
@@ -34,8 +18,6 @@ builder.Services.AddAGUIServer();
WebApplication app = builder.Build();
app.UseHttpLogging();
string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]
@@ -48,13 +30,12 @@ static string ApproveExpenseReport(string expenseReportId)
return $"Expense report {expenseReportId} approved";
}
// Get JsonSerializerOptions
var jsonOptions = app.Services.GetRequiredService<IOptions<JsonOptions>>().Value;
// Create approval-required tool
#pragma warning disable MEAI001 // Type is for evaluation purposes only
AITool[] tools = [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(ApproveExpenseReport))];
#pragma warning restore MEAI001
// Wrap the tool in ApprovalRequiredAIFunction so the run interrupts for approval before it executes.
AITool[] tools =
[
new ApprovalRequiredAIFunction(
AIFunctionFactory.Create(ApproveExpenseReport, name: "approve_expense_report"))
];
// Create base agent
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
@@ -70,8 +51,7 @@ ChatClientAgent baseAgent = openAIChatClient.AsAIAgent(
instructions: "You are a helpful assistant in charge of approving expenses",
tools: tools);
// Wrap with ServerFunctionApprovalAgent
var agent = new ServerFunctionApprovalAgent(baseAgent, jsonOptions.SerializerOptions);
app.MapAGUIServer("/", agent);
// No custom approval protocol is required: MapAGUIServer emits the approval interrupt natively when the
// model calls the approval-required tool, and resumes the run when the client sends the decision back.
app.MapAGUIServer("/", baseAgent);
await app.RunAsync();
@@ -1,249 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using ServerFunctionApproval;
/// <summary>
/// A delegating agent that handles function approval requests on the server side.
/// Transforms between ToolApprovalRequestContent/ToolApprovalResponseContent
/// and the request_approval tool call pattern for client communication.
/// </summary>
internal sealed class ServerFunctionApprovalAgent : DelegatingAIAgent
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
public ServerFunctionApprovalAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
: base(innerAgent)
{
this._jsonSerializerOptions = jsonSerializerOptions;
}
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
.ToAgentResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Process and transform incoming approval responses from client, creating a new message list
var processedMessages = ProcessIncomingFunctionApprovals(messages.ToList(), this._jsonSerializerOptions);
// Run the inner agent and intercept any approval requests
await foreach (var update in this.InnerAgent.RunStreamingAsync(
processedMessages, session, options, cancellationToken).ConfigureAwait(false))
{
yield return ProcessOutgoingApprovalRequests(update, this._jsonSerializerOptions);
}
}
#pragma warning disable MEAI001 // Type is for evaluation purposes only
private static ToolApprovalRequestContent ConvertToolCallToApprovalRequest(FunctionCallContent toolCall, JsonSerializerOptions jsonSerializerOptions)
{
if (toolCall.Name != "request_approval" || toolCall.Arguments == null)
{
throw new InvalidOperationException("Invalid request_approval tool call");
}
var request = (toolCall.Arguments.TryGetValue("request", out var reqObj) &&
reqObj is JsonElement argsElement &&
argsElement.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest))) is ApprovalRequest approvalRequest &&
approvalRequest != null ? approvalRequest : null) ?? throw new InvalidOperationException("Failed to deserialize approval request from tool call");
return new ToolApprovalRequestContent(
requestId: request.ApprovalId,
new FunctionCallContent(
callId: request.ApprovalId,
name: request.FunctionName,
arguments: request.FunctionArguments));
}
private static ToolApprovalResponseContent ConvertToolResultToApprovalResponse(FunctionResultContent result, ToolApprovalRequestContent approval, JsonSerializerOptions jsonSerializerOptions)
{
var approvalResponse = (result.Result is JsonElement je ?
(ApprovalResponse?)je.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) :
result.Result is string str ?
(ApprovalResponse?)JsonSerializer.Deserialize(str, jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) :
result.Result as ApprovalResponse) ?? throw new InvalidOperationException("Failed to deserialize approval response from tool result");
return approval.CreateResponse(approvalResponse.Approved);
}
#pragma warning restore MEAI001
private static List<ChatMessage> CopyMessagesUpToIndex(List<ChatMessage> messages, int index)
{
var result = new List<ChatMessage>(index);
for (int i = 0; i < index; i++)
{
result.Add(messages[i]);
}
return result;
}
private static List<AIContent> CopyContentsUpToIndex(IList<AIContent> contents, int index)
{
var result = new List<AIContent>(index);
for (int i = 0; i < index; i++)
{
result.Add(contents[i]);
}
return result;
}
private static List<ChatMessage> ProcessIncomingFunctionApprovals(
List<ChatMessage> messages,
JsonSerializerOptions jsonSerializerOptions)
{
List<ChatMessage>? result = null;
// Track approval ID to original call ID mapping
_ = new Dictionary<string, string>();
#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
Dictionary<string, ToolApprovalRequestContent> trackedRequestApprovalToolCalls = []; // Remote approvals
for (int messageIndex = 0; messageIndex < messages.Count; messageIndex++)
{
var message = messages[messageIndex];
List<AIContent>? transformedContents = null;
for (int j = 0; j < message.Contents.Count; j++)
{
var content = message.Contents[j];
if (content is FunctionCallContent { Name: "request_approval" } toolCall)
{
result ??= CopyMessagesUpToIndex(messages, messageIndex);
transformedContents ??= CopyContentsUpToIndex(message.Contents, j);
var approvalRequest = ConvertToolCallToApprovalRequest(toolCall, jsonSerializerOptions);
transformedContents.Add(approvalRequest);
trackedRequestApprovalToolCalls[toolCall.CallId] = approvalRequest;
result.Add(new ChatMessage(message.Role, transformedContents)
{
AuthorName = message.AuthorName,
MessageId = message.MessageId,
CreatedAt = message.CreatedAt,
RawRepresentation = message.RawRepresentation,
AdditionalProperties = message.AdditionalProperties
});
}
else if (content is FunctionResultContent toolResult &&
trackedRequestApprovalToolCalls.TryGetValue(toolResult.CallId, out var approval))
{
result ??= CopyMessagesUpToIndex(messages, messageIndex);
transformedContents ??= CopyContentsUpToIndex(message.Contents, j);
var approvalResponse = ConvertToolResultToApprovalResponse(toolResult, approval, jsonSerializerOptions);
transformedContents.Add(approvalResponse);
result.Add(new ChatMessage(message.Role, transformedContents)
{
AuthorName = message.AuthorName,
MessageId = message.MessageId,
CreatedAt = message.CreatedAt,
RawRepresentation = message.RawRepresentation,
AdditionalProperties = message.AdditionalProperties
});
}
else
{
result?.Add(message);
}
}
}
#pragma warning restore MEAI001
return result ?? messages;
}
private static AgentResponseUpdate ProcessOutgoingApprovalRequests(
AgentResponseUpdate update,
JsonSerializerOptions jsonSerializerOptions)
{
IList<AIContent>? updatedContents = null;
for (var i = 0; i < update.Contents.Count; i++)
{
var content = update.Contents[i];
#pragma warning disable MEAI001 // Type is for evaluation purposes only
if (content is ToolApprovalRequestContent request && request.ToolCall is FunctionCallContent functionCall)
{
updatedContents ??= [.. update.Contents];
var approvalId = request.RequestId;
var approvalData = new ApprovalRequest
{
ApprovalId = approvalId,
FunctionName = functionCall.Name,
FunctionArguments = functionCall.Arguments,
Message = $"Approve execution of '{functionCall.Name}'?"
};
updatedContents[i] = new FunctionCallContent(
callId: approvalId,
name: "request_approval",
arguments: new Dictionary<string, object?> { ["request"] = approvalData });
}
#pragma warning restore MEAI001
}
if (updatedContents is not null)
{
var chatUpdate = update.AsChatResponseUpdate();
// Yield a tool call update that represents the approval request
return new AgentResponseUpdate(new ChatResponseUpdate()
{
Role = chatUpdate.Role,
Contents = updatedContents,
MessageId = chatUpdate.MessageId,
AuthorName = chatUpdate.AuthorName,
CreatedAt = chatUpdate.CreatedAt,
RawRepresentation = chatUpdate.RawRepresentation,
ResponseId = chatUpdate.ResponseId,
AdditionalProperties = chatUpdate.AdditionalProperties
})
{
AgentId = update.AgentId,
ContinuationToken = update.ContinuationToken
};
}
return update;
}
}
namespace ServerFunctionApproval
{
// Define approval models
public sealed class ApprovalRequest
{
[JsonPropertyName("approval_id")]
public required string ApprovalId { get; init; }
[JsonPropertyName("function_name")]
public required string FunctionName { get; init; }
[JsonPropertyName("function_arguments")]
public IDictionary<string, object?>? FunctionArguments { get; init; }
[JsonPropertyName("message")]
public string? Message { get; init; }
}
public sealed class ApprovalResponse
{
[JsonPropertyName("approval_id")]
public required string ApprovalId { get; init; }
[JsonPropertyName("approved")]
public required bool Approved { get; init; }
}
[JsonSerializable(typeof(ApprovalRequest))]
[JsonSerializable(typeof(ApprovalResponse))]
[JsonSerializable(typeof(Dictionary<string, object?>))]
public sealed partial class ApprovalJsonContext : JsonSerializerContext;
}
@@ -20,16 +20,15 @@ using HttpClient httpClient = new()
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
AIAgent baseAgent = chatClient.AsAIAgent(
AIAgent agent = chatClient.AsAIAgent(
name: "recipe-client",
description: "AG-UI Recipe Client Agent");
// Wrap the base agent with state management
JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web)
{
TypeInfoResolver = RecipeSerializerContext.Default
};
StatefulAgent<AgentState> agent = new(baseAgent, jsonOptions, new AgentState());
JsonSerializerOptions jsonOptions = RecipeSerializerContext.Default.Options;
// The recipe lives on the client. It is sent to the server on every turn (so the agent edits the
// existing recipe) and refreshed from each STATE_SNAPSHOT the server streams back.
Recipe currentRecipe = new();
AgentSession session = await agent.CreateSessionAsync();
List<ChatMessage> messages =
@@ -42,7 +41,7 @@ try
while (true)
{
// Get user input
Console.Write("\nUser (:q to quit, :state to show state): ");
Console.Write("\nUser (:q to quit, :state to show recipe): ");
string? message = Console.ReadLine();
if (string.IsNullOrWhiteSpace(message))
@@ -58,36 +57,51 @@ try
if (message.Equals(":state", StringComparison.OrdinalIgnoreCase))
{
DisplayState(agent.State.Recipe);
DisplayRecipe(currentRecipe);
continue;
}
messages.Add(new ChatMessage(ChatRole.User, message));
// Send the client's current recipe on the AG-UI RunAgentInput.State so the agent builds on it.
JsonElement stateJson = JsonSerializer.SerializeToElement(
new RecipeResponse { Recipe = currentRecipe }, jsonOptions);
ChatClientAgentRunOptions runOptions = new()
{
ChatOptions = new ChatOptions
{
RawRepresentationFactory = _ => new RunAgentInput { State = stateJson }
}
};
// Stream the response
bool isFirstUpdate = true;
string? threadId = null;
bool stateReceived = false;
Console.WriteLine();
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session, runOptions))
{
ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate();
// First update indicates run started
if (isFirstUpdate)
{
// AGUIChatClient is stateless and never surfaces a ConversationId; the thread
// id is carried on the AG-UI RUN_STARTED event's raw representation.
threadId = (chatUpdate.RawRepresentation as RunStartedEvent)?.ThreadId;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
Console.WriteLine($"[Run Started - Run: {chatUpdate.ResponseId}]");
Console.ResetColor();
isFirstUpdate = false;
}
// Display streaming content
// A STATE_SNAPSHOT arrives as a StateSnapshotEvent on the update's raw representation.
if (chatUpdate.RawRepresentation is StateSnapshotEvent snapshot &&
snapshot.Snapshot.Deserialize<RecipeResponse>(jsonOptions) is { } response)
{
currentRecipe = response.Recipe;
Console.ForegroundColor = ConsoleColor.Blue;
Console.WriteLine("\n[State Snapshot Received]");
Console.ResetColor();
}
// Display streaming text content
foreach (AIContent content in update.Contents)
{
switch (content)
@@ -98,14 +112,6 @@ try
Console.ResetColor();
break;
case DataContent dataContent when dataContent.MediaType == "application/json":
// This is a state snapshot - the StatefulAgent has already updated the state
stateReceived = true;
Console.ForegroundColor = ConsoleColor.Blue;
Console.WriteLine("\n[State Snapshot Received]");
Console.ResetColor();
break;
case ErrorContent errorContent:
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"\n[Error: {errorContent.Message}]");
@@ -115,15 +121,14 @@ try
}
}
// The session owns prior history, so the next run sends only the new user message.
messages.Clear();
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
Console.WriteLine("\n[Run Finished]");
Console.ResetColor();
// Display final state if received
if (stateReceived)
{
DisplayState(agent.State.Recipe);
}
DisplayRecipe(currentRecipe);
}
}
catch (Exception ex)
@@ -131,61 +136,53 @@ catch (Exception ex)
Console.WriteLine($"\nAn error occurred: {ex.Message}");
}
static void DisplayState(RecipeState? state)
static void DisplayRecipe(Recipe recipe)
{
if (state == null)
{
Console.ForegroundColor = ConsoleColor.Gray;
Console.WriteLine("\n[No state available]");
Console.ResetColor();
return;
}
Console.ForegroundColor = ConsoleColor.Blue;
Console.WriteLine("\n" + new string('=', 60));
Console.WriteLine("CURRENT STATE");
Console.WriteLine("CURRENT RECIPE");
Console.WriteLine(new string('=', 60));
Console.ResetColor();
if (!string.IsNullOrEmpty(state.Title))
if (string.IsNullOrEmpty(recipe.Title))
{
Console.WriteLine("\nRecipe:");
Console.WriteLine($" Title: {state.Title}");
if (!string.IsNullOrEmpty(state.Cuisine))
Console.ForegroundColor = ConsoleColor.Gray;
Console.WriteLine("\n[No recipe yet]");
Console.ResetColor();
}
else
{
Console.WriteLine($"\n Title: {recipe.Title}");
if (!string.IsNullOrEmpty(recipe.SkillLevel))
{
Console.WriteLine($" Cuisine: {state.Cuisine}");
Console.WriteLine($" Skill Level: {recipe.SkillLevel}");
}
if (!string.IsNullOrEmpty(state.SkillLevel))
if (!string.IsNullOrEmpty(recipe.CookingTime))
{
Console.WriteLine($" Skill Level: {state.SkillLevel}");
Console.WriteLine($" Cooking Time: {recipe.CookingTime}");
}
if (state.PrepTimeMinutes > 0)
if (recipe.SpecialPreferences.Count > 0)
{
Console.WriteLine($" Prep Time: {state.PrepTimeMinutes} minutes");
Console.WriteLine($" Preferences: {string.Join(", ", recipe.SpecialPreferences)}");
}
if (state.CookTimeMinutes > 0)
{
Console.WriteLine($" Cook Time: {state.CookTimeMinutes} minutes");
}
if (state.Ingredients.Count > 0)
if (recipe.Ingredients.Count > 0)
{
Console.WriteLine("\n Ingredients:");
foreach (var ingredient in state.Ingredients)
foreach (Ingredient ingredient in recipe.Ingredients)
{
Console.WriteLine($" - {ingredient}");
Console.WriteLine($" {ingredient.Icon} {ingredient.Name} - {ingredient.Amount}");
}
}
if (state.Steps.Count > 0)
if (recipe.Instructions.Count > 0)
{
Console.WriteLine("\n Steps:");
for (int i = 0; i < state.Steps.Count; i++)
Console.WriteLine("\n Instructions:");
for (int i = 0; i < recipe.Instructions.Count; i++)
{
Console.WriteLine($" {i + 1}. {state.Steps[i]}");
Console.WriteLine($" {i + 1}. {recipe.Instructions[i]}");
}
}
}
@@ -195,40 +192,53 @@ static void DisplayState(RecipeState? state)
Console.ResetColor();
}
// State wrapper
internal sealed class AgentState
namespace RecipeClient
{
[JsonPropertyName("recipe")]
public RecipeState Recipe { get; set; } = new();
// State response wrapper. Its shape mirrors what the server returns and renders as state.
internal sealed class RecipeResponse
{
[JsonPropertyName("recipe")]
public Recipe Recipe { get; set; } = new();
}
// Recipe state model.
internal sealed class Recipe
{
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("skill_level")]
public string SkillLevel { get; set; } = string.Empty;
[JsonPropertyName("cooking_time")]
public string CookingTime { get; set; } = string.Empty;
[JsonPropertyName("special_preferences")]
public List<string> SpecialPreferences { get; set; } = [];
[JsonPropertyName("ingredients")]
public List<Ingredient> Ingredients { get; set; } = [];
[JsonPropertyName("instructions")]
public List<string> Instructions { get; set; } = [];
}
// A single ingredient.
internal sealed class Ingredient
{
[JsonPropertyName("icon")]
public string Icon { get; set; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("amount")]
public string Amount { get; set; } = string.Empty;
}
// JSON serialization context.
[JsonSerializable(typeof(RecipeResponse))]
[JsonSerializable(typeof(Recipe))]
[JsonSerializable(typeof(Ingredient))]
internal sealed partial class RecipeSerializerContext : JsonSerializerContext;
}
// Recipe state model
internal sealed class RecipeState
{
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("cuisine")]
public string Cuisine { get; set; } = string.Empty;
[JsonPropertyName("ingredients")]
public List<string> Ingredients { get; set; } = [];
[JsonPropertyName("steps")]
public List<string> Steps { get; set; } = [];
[JsonPropertyName("prep_time_minutes")]
public int PrepTimeMinutes { get; set; }
[JsonPropertyName("cook_time_minutes")]
public int CookTimeMinutes { get; set; }
[JsonPropertyName("skill_level")]
public string SkillLevel { get; set; } = string.Empty;
}
// JSON serialization context
[JsonSerializable(typeof(AgentState))]
[JsonSerializable(typeof(RecipeState))]
[JsonSerializable(typeof(JsonElement))]
internal sealed partial class RecipeSerializerContext : JsonSerializerContext;
@@ -1,87 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace RecipeClient;
/// <summary>
/// A delegating agent that manages client-side state and automatically attaches it to requests.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
internal sealed class StatefulAgent<TState> : DelegatingAIAgent
where TState : class, new()
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
/// <summary>
/// Gets or sets the current state.
/// </summary>
public TState State { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="StatefulAgent{TState}"/> class.
/// </summary>
/// <param name="innerAgent">The underlying agent to delegate to.</param>
/// <param name="jsonSerializerOptions">The JSON serializer options for state serialization.</param>
/// <param name="initialState">The initial state. If null, a new instance will be created.</param>
public StatefulAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions, TState? initialState = null)
: base(innerAgent)
{
this._jsonSerializerOptions = jsonSerializerOptions;
this.State = initialState ?? new TState();
}
/// <inheritdoc />
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
.ToAgentResponseAsync(cancellationToken);
}
/// <inheritdoc />
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Add state to messages
List<ChatMessage> messagesWithState = [.. messages];
// Serialize the state using AgentState wrapper
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
this.State,
this._jsonSerializerOptions.GetTypeInfo(typeof(TState)));
DataContent stateContent = new(stateBytes, "application/json");
ChatMessage stateMessage = new(ChatRole.System, [stateContent]);
messagesWithState.Add(stateMessage);
// Stream the response and update state when received
await foreach (AgentResponseUpdate update in this.InnerAgent.RunStreamingAsync(messagesWithState, session, options, cancellationToken))
{
// Check if this update contains a state snapshot
foreach (AIContent content in update.Contents)
{
if (content is DataContent dataContent && dataContent.MediaType == "application/json")
{
// Deserialize the state
if (JsonSerializer.Deserialize(
dataContent.Data.Span,
this._jsonSerializerOptions.GetTypeInfo(typeof(TState))) is TState newState)
{
this.State = newState;
}
}
}
yield return update;
}
}
}
@@ -1,15 +1,16 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using AGUI.Server;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
using RecipeAssistant;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.TypeInfoResolverChain.Add(RecipeSerializerContext.Default));
builder.Services.AddAGUIServer();
@@ -29,10 +30,32 @@ string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]
?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");
// Get JsonSerializerOptions
var jsonOptions = app.Services.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>().Value;
// The tool returns the complete recipe. The hosting layer turns each result into a STATE_SNAPSHOT
// event via AGUIStreamOptions.MapResultAsStateSnapshot("generate_recipe") - no protocol content by hand.
[Description("Generate or update the shared recipe and display it to the user.")]
static RecipeResponse GenerateRecipe(
[Description("The complete recipe to display.")] Recipe recipe) => new() { Recipe = recipe };
// Create base agent
AITool generateRecipe = AIFunctionFactory.Create(
GenerateRecipe,
name: "generate_recipe",
description: "Generate or update the shared recipe and display it to the user.",
RecipeSerializerContext.Default.Options);
const string SharedStateSystemPrompt =
"""
You are a helpful recipe assistant that maintains a shared recipe state with the user.
IMPORTANT:
- When the user asks you to create, change, or improve a recipe, call the `generate_recipe`
tool with a COMPLETE recipe: a title, skill_level, cooking_time, special_preferences, the
full list of ingredients (each with an icon, name and amount) and the step-by-step
instructions.
- Always include every ingredient the recipe needs, keeping any the user already added.
- When the user only asks a question about the recipe, answer in plain text and do NOT call the tool.
""";
// Create the AI agent with the recipe tool.
// 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.
@@ -41,26 +64,22 @@ ChatClient chatClient = new AzureOpenAIClient(
new DefaultAzureCredential())
.GetChatClient(deploymentName);
AIAgent baseAgent = chatClient.AsAIAgent(
name: "RecipeAgent",
instructions: """
You are a helpful recipe assistant. When users ask you to create or suggest a recipe,
respond with a complete AgentState JSON object that includes:
- recipe.title: The recipe name
- recipe.cuisine: Type of cuisine (e.g., Italian, Mexican, Japanese)
- recipe.ingredients: Array of ingredient strings with quantities
- recipe.steps: Array of cooking instruction strings
- recipe.prep_time_minutes: Preparation time in minutes
- recipe.cook_time_minutes: Cooking time in minutes
- recipe.skill_level: One of "beginner", "intermediate", or "advanced"
AIAgent baseAgent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
Name = "RecipeAgent",
Description = "An agent that maintains a shared recipe state with the user.",
ChatOptions = new ChatOptions
{
Instructions = SharedStateSystemPrompt,
Tools = [generateRecipe],
},
});
Always include all fields in the response. Be creative and helpful.
""");
// Wrap with a thin agent that injects the client's current recipe (input side of shared state).
AIAgent agent = new RecipeStateAgent(baseAgent);
// Wrap with state management middleware
AIAgent agent = new SharedStateAgent(baseAgent, jsonOptions.SerializerOptions);
// Map the AG-UI agent endpoint
app.MapAGUIServer("/", agent);
// Map the AG-UI endpoint. A generate_recipe result becomes a STATE_SNAPSHOT event (output side).
app.MapAGUIServer("/", agent)
.WithMetadata(new AGUIStreamOptions().MapResultAsStateSnapshot("generate_recipe"));
await app.RunAsync();
@@ -4,40 +4,50 @@ using System.Text.Json.Serialization;
namespace RecipeAssistant;
// State wrapper
internal sealed class AgentState
// State response wrapper returned by the tool. Its shape is what the client renders as state.
internal sealed class RecipeResponse
{
[JsonPropertyName("recipe")]
public RecipeState Recipe { get; set; } = new();
public Recipe Recipe { get; set; } = new();
}
// Recipe state model
internal sealed class RecipeState
// Recipe state model.
internal sealed class Recipe
{
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("cuisine")]
public string Cuisine { get; set; } = string.Empty;
[JsonPropertyName("ingredients")]
public List<string> Ingredients { get; set; } = [];
[JsonPropertyName("steps")]
public List<string> Steps { get; set; } = [];
[JsonPropertyName("prep_time_minutes")]
public int PrepTimeMinutes { get; set; }
[JsonPropertyName("cook_time_minutes")]
public int CookTimeMinutes { get; set; }
[JsonPropertyName("skill_level")]
public string SkillLevel { get; set; } = string.Empty;
[JsonPropertyName("cooking_time")]
public string CookingTime { get; set; } = string.Empty;
[JsonPropertyName("special_preferences")]
public List<string> SpecialPreferences { get; set; } = [];
[JsonPropertyName("ingredients")]
public List<Ingredient> Ingredients { get; set; } = [];
[JsonPropertyName("instructions")]
public List<string> Instructions { get; set; } = [];
}
// JSON serialization context
[JsonSerializable(typeof(AgentState))]
[JsonSerializable(typeof(RecipeState))]
[JsonSerializable(typeof(System.Text.Json.JsonElement))]
// A single ingredient.
internal sealed class Ingredient
{
[JsonPropertyName("icon")]
public string Icon { get; set; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("amount")]
public string Amount { get; set; } = string.Empty;
}
// JSON serialization context for the tool payloads.
[JsonSerializable(typeof(RecipeResponse))]
[JsonSerializable(typeof(Recipe))]
[JsonSerializable(typeof(Ingredient))]
internal sealed partial class RecipeSerializerContext : JsonSerializerContext;
@@ -0,0 +1,46 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using AGUI.Abstractions;
using AGUI.Server;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace RecipeAssistant;
/// <summary>
/// A thin agent that reads the client's current recipe from the AG-UI <see cref="RunAgentInput.State"/>
/// and prepends it to the conversation as a system message, so the model edits the existing recipe
/// instead of starting over. This handles the input side of shared state only. The output side is
/// declarative: the inner agent's <c>generate_recipe</c> tool result becomes a <c>STATE_SNAPSHOT</c>
/// via <c>AGUIStreamOptions.MapResultAsStateSnapshot</c>.
/// </summary>
internal sealed class RecipeStateAgent(AIAgent innerAgent) : DelegatingAIAgent(innerAgent)
{
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
.ToAgentResponseAsync(cancellationToken);
protected override IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
if (options is ChatClientAgentRunOptions { ChatOptions: { } chatOptions } &&
chatOptions.TryGetRunAgentInput(out RunAgentInput? input) &&
input.State is { ValueKind: JsonValueKind.Object } state)
{
ChatMessage stateMessage = new(
ChatRole.System,
$"The user's current recipe state is:\n{state.GetRawText()}");
messages = [stateMessage, .. messages];
}
return this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken);
}
}
@@ -1,159 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Runtime.CompilerServices;
using System.Text.Json;
using AGUI.Abstractions;
using AGUI.Server;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace RecipeAssistant;
internal sealed class SharedStateAgent : DelegatingAIAgent
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
public SharedStateAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
: base(innerAgent)
{
this._jsonSerializerOptions = jsonSerializerOptions;
}
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
return this.RunCoreStreamingAsync(messages, session, options, cancellationToken)
.ToAgentResponseAsync(cancellationToken);
}
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Check if the client sent state in the request
if (options is not ChatClientAgentRunOptions { ChatOptions: { } chatOptions } chatRunOptions ||
!chatOptions.TryGetRunAgentInput(out RunAgentInput? agentInput) ||
agentInput.State is not { ValueKind: JsonValueKind.Object } state)
{
// No state management requested, pass through to inner agent
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
yield break;
}
// Check if state has properties (not empty {})
bool hasProperties = false;
foreach (JsonProperty _ in state.EnumerateObject())
{
hasProperties = true;
break;
}
if (!hasProperties)
{
// Empty state - treat as no state
await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
yield break;
}
// First run: Generate structured state update
var firstRunOptions = new ChatClientAgentRunOptions
{
ChatOptions = chatRunOptions.ChatOptions.Clone(),
AllowBackgroundResponses = chatRunOptions.AllowBackgroundResponses,
ContinuationToken = chatRunOptions.ContinuationToken,
ChatClientFactory = chatRunOptions.ChatClientFactory,
};
// Configure JSON schema response format for structured state output
firstRunOptions.ChatOptions.ResponseFormat = ChatResponseFormat.ForJsonSchema<AgentState>(
schemaName: "AgentState",
schemaDescription: "A response containing a recipe with title, skill level, cooking time, ingredients, and instructions");
// Add current state to the conversation - state is already a JsonElement
ChatMessage stateUpdateMessage = new(
ChatRole.System,
[
new TextContent("Here is the current state in JSON format:"),
new TextContent(JsonSerializer.Serialize(state, this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)))),
new TextContent("The new state is:")
]);
var firstRunMessages = messages.Append(stateUpdateMessage);
// Collect all updates from first run
var allUpdates = new List<AgentResponseUpdate>();
await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, session, firstRunOptions, cancellationToken).ConfigureAwait(false))
{
allUpdates.Add(update);
// Yield all non-text updates (tool calls, etc.)
bool hasNonTextContent = update.Contents.Any(c => c is not TextContent);
if (hasNonTextContent)
{
yield return update;
}
}
var response = allUpdates.ToAgentResponse();
// Try to deserialize the structured state response
if (TryDeserialize(response.Text, this._jsonSerializerOptions, out JsonElement stateSnapshot))
{
// Serialize and emit as STATE_SNAPSHOT via DataContent
byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
stateSnapshot,
this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));
yield return new AgentResponseUpdate
{
Contents = [new DataContent(stateBytes, "application/json")]
};
}
else
{
yield break;
}
// Second run: Generate user-friendly summary
var secondRunMessages = messages.Concat(response.Messages).Append(
new ChatMessage(
ChatRole.System,
[new TextContent("Please provide a concise summary of the state changes in at most two sentences.")]));
await foreach (var update in this.InnerAgent.RunStreamingAsync(secondRunMessages, session, options, cancellationToken).ConfigureAwait(false))
{
yield return update;
}
}
private static bool TryDeserialize<T>(string json, JsonSerializerOptions jsonSerializerOptions, out T structuredOutput)
{
try
{
T? deserialized = JsonSerializer.Deserialize<T>(json, jsonSerializerOptions);
if (deserialized is null)
{
structuredOutput = default!;
return false;
}
structuredOutput = deserialized;
return true;
}
catch
{
structuredOutput = default!;
return false;
}
}
}
@@ -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.3.0" />
<PackageReference Include="AgentMemory.AgentFramework" Version="1.3.0" />
<PackageReference Include="AgentMemory" Version="1.4.1" />
<PackageReference Include="AgentMemory.AgentFramework" Version="1.4.1" />
<!-- 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" />
@@ -14,6 +14,58 @@ This sample demonstrates how to use a `CompactionProvider` with a `PipelineCompa
## Concepts
### Choosing between `CompactionProvider` and `IChatReducer`
Both abstractions reduce the messages sent to a model, but they run at different layers and have different effects on stored history.
| Choose | When you need | Effect on stored history | Function-calling loop |
|---|---|---|---|
| `CompactionProvider` on `ChatClientBuilder.UseAIContextProviders(...)` | Request-context management that preserves the original conversation | The compacted view is forwarded to the inner chat client; the source history remains unchanged | Runs for each inner chat-client call, including calls made while tools are being invoked |
| `CompactionProvider` in `ChatClientAgentOptions.AIContextProviders` | Agent-specific compaction without decorating a shared chat client | Runs before chat history is stored, so generated replacement messages can become part of the persisted history | Runs at the agent boundary, not for each call inside the tool loop |
| `IChatReducer` in `InMemoryChatHistoryProviderOptions.ChatReducer` | Storage management where the reduced list should replace the session's in-memory history | Permanently replaces the provider's stored message list with the reducer output | Runs at the configured history-provider event, not for each call inside the tool loop |
Use a builder-level `CompactionProvider` when the primary goal is to fit each model request within a context window while retaining the complete conversation for auditing, replay, or a different downstream policy. Use an `IChatReducer` when the primary goal is to bound the history retained in `InMemoryChatHistoryProvider` itself. If the reduced history is serialized with the session, the discarded messages are no longer present after the session is restored.
`InMemoryChatHistoryProvider` can run its reducer at either of these events:
- `BeforeMessagesRetrieval` (the default) reduces stored history immediately before it is supplied to the agent.
- `AfterMessageAdded` reduces stored history after each request/response pair is added.
The event controls *when* reduction occurs; the `IChatReducer` implementation controls *how* messages are reduced. By contrast, a `CompactionStrategy` supplies its own `CompactionTrigger` and operates on message groups that preserve tool-call/result pairs.
#### Adapting between the abstractions
The adapters support existing implementations at either integration point. Pick the direction that matches the layer where you want reduction to run.
To use a `CompactionStrategy` for persistent in-memory history reduction, adapt it to `IChatReducer`:
```csharp
CompactionStrategy strategy =
new SlidingWindowCompactionStrategy(CompactionTriggers.TurnsExceed(20));
InMemoryChatHistoryProviderOptions historyOptions = new()
{
ChatReducer = strategy.AsChatReducer(),
ReducerTriggerEvent = InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval
};
InMemoryChatHistoryProvider historyProvider = new(historyOptions);
```
To use an existing `IChatReducer` in a compaction pipeline or for in-run request compaction, adapt it to `CompactionStrategy`:
```csharp
IChatReducer existingReducer = /* your MEAI reducer */;
CompactionStrategy strategy = new ChatReducerCompactionStrategy(
existingReducer,
CompactionTriggers.TokensExceed(4000));
CompactionProvider provider = new(strategy);
```
Do not wrap a strategy with `AsChatReducer()` and immediately wrap that reducer in `ChatReducerCompactionStrategy`. That round trip adds no capability; choose the original strategy directly and register it at the appropriate layer.
### Message groups
The compaction engine organizes messages into atomic *groups* that are treated as indivisible units during compaction. A group is either:
@@ -0,0 +1,16 @@
<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" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,108 @@
// Copyright (c) Microsoft. All rights reserved.
// Multi-Model Routing — Switch the model an agent talks to, mid-conversation
//
// This sample shows how to use the RoutePersistingRoutingChatClient to route each agent turn to one of
// several named chat clients (routes), where the route that is active for a session is persisted
// in the session's state bag.
//
// Because the conversation history is kept client side by the agent's chat history provider, the
// full history is replayed to whichever model handles the next turn. Switching route therefore
// preserves the conversation — no manual rehydration is required.
//
// The sample runs a simple interactive loop. In addition to chatting with the agent, you can:
// /route — show the route that is currently active for the session
// /route <name> — switch the session to the named route
// /exit — quit (an empty line also exits)
using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var primaryModel = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
var secondaryModel = Environment.GetEnvironmentVariable("FOUNDRY_MODEL_ALTERNATE") ?? "gpt-5.4";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
var responsesClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.GetProjectOpenAIClient()
.GetProjectResponsesClient();
// <create_routing_client>
// Each route is an ordinary IChatClient. Here both routes target the same project but a different
// model deployment; they could equally be clients for entirely different providers.
// Stored output is disabled so that the conversation is carried client side and can be replayed
// against whichever model handles the next turn.
var routingClient = new RoutePersistingRoutingChatClient(
new Dictionary<string, IChatClient>
{
[primaryModel] = responsesClient.AsIChatClientWithStoredOutputDisabled(primaryModel),
},
new RoutePersistingRoutingChatClientOptions { DefaultRoute = primaryModel });
// Routes remain mutable after construction. Add, replace, or remove entries only when no requests are in flight.
routingClient.Routes[secondaryModel] = responsesClient.AsIChatClientWithStoredOutputDisabled(secondaryModel);
// </create_routing_client>
AIAgent agent = routingClient.AsAIAgent(new ChatClientAgentOptions
{
Name = "Router",
ChatOptions = new() { Instructions = "You are a helpful assistant. Always state which model you are when asked." },
// Keep the conversation client side so it survives a route change.
ChatHistoryProvider = new InMemoryChatHistoryProvider(),
});
AgentSession session = await agent.CreateSessionAsync();
Console.WriteLine($"Routes: {string.Join(", ", routingClient.Routes.Keys)}");
Console.WriteLine($"Active route: {routingClient.GetActiveRoute(session)}");
Console.WriteLine("Type a message, '/route <name>' to switch model, or '/exit' to quit.");
while (true)
{
Console.Write("\nYou > ");
var input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input) || input.Equals("/exit", StringComparison.OrdinalIgnoreCase))
{
break;
}
if (input.StartsWith("/route", StringComparison.OrdinalIgnoreCase))
{
HandleRouteCommand(input);
continue;
}
var response = await agent.RunAsync(input, session);
Console.WriteLine($"\n[{routingClient.GetActiveRoute(session)}] Agent > {response}");
}
// <switch_route>
// Reading and changing the active route for a session. The new route is persisted in the session's
// state bag, so it applies to every subsequent turn of that session.
void HandleRouteCommand(string input)
{
var requested = input.Length > "/route".Length ? input["/route".Length..].Trim() : string.Empty;
if (requested.Length == 0)
{
Console.WriteLine($"Active route: {routingClient.GetActiveRoute(session)}");
return;
}
if (!routingClient.Routes.ContainsKey(requested))
{
Console.WriteLine($"Unknown route '{requested}'. Available: {string.Join(", ", routingClient.Routes.Keys)}");
return;
}
routingClient.SetActiveRoute(session, requested);
Console.WriteLine($"Switched to route: {requested}");
}
// </switch_route>
@@ -0,0 +1,61 @@
# Multi-Model Routing
This sample demonstrates how to use the `RoutePersistingRoutingChatClient` to route each agent turn to
one of several named chat clients (routes), and to switch the active route mid-conversation without
losing the conversation history.
The `RoutePersistingRoutingChatClient` derives from the `RoutingChatClient` in `Microsoft.Extensions.AI`
and stores the route that is active for a session in the session's state bag. The selection
therefore survives for the lifetime of the session and across session serialization.
Because the conversation history is kept client side by the agent's chat history provider, the full
history is replayed to whichever model handles the next turn, so switching route preserves the
conversation.
> [!WARNING]
> Ensure that none of the chat clients registered as routes use service-stored chat history.
> Routing relies on the agent keeping history client side so it can share the complete conversation
> with whichever client handles the next turn. Service-stored history is isolated to its originating
> service and cannot be shared across routes, so switching routes would lose conversation context.
## What it demonstrates
- Registering multiple named routes, each backed by an ordinary `IChatClient`.
- Adding, replacing, or removing routes after constructing the routing client.
- Choosing the route a new session starts on with `RoutePersistingRoutingChatClientOptions.DefaultRoute`.
- Reading the active route for a session with `GetActiveRoute`.
- Changing the active route for a session with `SetActiveRoute`.
- Preserving the conversation across a route change by keeping chat history client side.
## Commands
| Command | Description |
|---|---|
| `/route` | Show the route that is currently active for the session |
| `/route <name>` | Switch the session to the named route |
| `/exit` | Quit (an empty line also exits) |
## Configuration
| Environment variable | Required | Description |
|---|---|---|
| `FOUNDRY_PROJECT_ENDPOINT` | Yes | The Foundry project endpoint. |
| `FOUNDRY_MODEL` | No | The primary model deployment name. Defaults to `gpt-5.4-mini`. |
| `FOUNDRY_MODEL_ALTERNATE` | No | The secondary model deployment name. Defaults to `gpt-5.4`. |
## Running the sample
```bash
export FOUNDRY_PROJECT_ENDPOINT="<your-foundry-project-endpoint>"
dotnet run
```
## Notes
- The routing client resolves the session from the ambient agent run context, so it must be invoked
as part of an `AIAgent.RunAsync` or `AIAgent.RunStreamingAsync` call.
- The `Routes` dictionary is mutable but is not thread-safe. Modify it only while no requests are in
flight. Route entries are validated only when selected, so an unused incomplete entry does not
prevent other routes from operating.
- For routing policies that are not persisted per session, such as content-based or failover
routing, use the routing clients provided by `Microsoft.Extensions.AI` directly.
+2 -1
View File
@@ -44,12 +44,13 @@ Before you begin, ensure you have the following prerequisites:
|[Deep research with an agent](./Agent_Step15_DeepResearch/)|This sample demonstrates how to use the Deep Research Tool to perform comprehensive research on complex topics|
|[Declarative agent](./Agent_Step16_Declarative/)|This sample demonstrates how to declaratively define an agent.|
|[Providing additional AI Context to an agent using multiple AIContextProviders](./Agent_Step17_AdditionalAIContext/)|This sample demonstrates how to inject additional AI context into a ChatClientAgent using multiple custom AIContextProvider components that are attached to the agent.|
|[Using compaction pipeline with an agent](./Agent_Step18_CompactionPipeline/)|This sample demonstrates how to use a compaction pipeline to efficiently limit the size of the conversation history for an agent.|
|[Using compaction pipeline with an agent](./Agent_Step18_CompactionPipeline/)|This sample demonstrates how to use a compaction pipeline and how to choose between request-level `CompactionProvider` and persistent-history `IChatReducer` integration.|
|[In-function-loop checkpointing](./Agent_Step19_InFunctionLoopCheckpointing/)|This sample demonstrates how to persist chat history after each service call during a tool-calling loop, enabling crash recovery and mid-run observability.|
|[Dynamic function tools](./Agent_Step20_DynamicFunctionTools/)|This sample demonstrates how to dynamically expand the set of function tools available to an agent during a function-calling loop using the ambient FunctionInvocationContext.|
|[Shell tool with environment-aware system prompt](./Agent_Step21_ShellWithEnvironment/)|This sample demonstrates how to use the shell tool together with the ShellEnvironmentProvider to run commands in stateless and persistent modes, injecting environment-aware instructions so the agent emits commands in the right shell idiom.|
|[Switching agent operating mode](./Agent_Step22_AgentMode/)|This sample demonstrates how to use the AgentModeProvider to track and switch an agent's operating mode at runtime, including the built-in plan/execute modes and custom modes, with a simple input loop that switches mode using a slash command.|
|[Tracking work with a todo list](./Agent_Step23_TodoList/)|This sample demonstrates how to use the TodoProvider to let an agent plan and track multi-step work using a todo list that persists across turns, printing the evolving todo list after each turn.|
|[Routing turns across multiple models](./Agent_Step24_MultiModelRouting/)|This sample demonstrates how to use the RoutePersistingRoutingChatClient to route each agent turn to one of several named chat clients, switching the active model mid-conversation while preserving the conversation history.|
## Running the samples from the console
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);OPENAI001;MAAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="OpenTelemetry.Exporter.Console" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ClawAgent\ClawAgent.csproj" />
<ProjectReference Include="..\..\..\Harness_Shared_Console\Harness_Shared_Console.csproj" />
<ProjectReference Include="..\..\..\Harness_Shared_Console_OpenAI\Harness_Shared_Console_OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.Metrics;
using ClawAgent;
using Harness.Shared.Console;
using Harness.Shared.Console.OpenAI;
using Harness.Shared.Console.ToolFormatters;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
const string ServiceName = "ClawAgent.Console";
var otlpEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT");
var telemetryEnabled = !string.IsNullOrWhiteSpace(otlpEndpoint);
// Export telemetry only when an OTLP endpoint is configured. We deliberately avoid the
// console exporter: this is an interactive app whose UI is rendered by
// HarnessConsole.RunAgentAsync, and streaming spans/metrics to stdout corrupts that UI.
var resourceBuilder = ResourceBuilder.CreateDefault().AddService(ServiceName, serviceVersion: "1.0.0");
using var tracerProvider = telemetryEnabled
? Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(resourceBuilder)
.AddSource(ClawAgentFactory.OpenTelemetrySourceName)
.AddHttpClientInstrumentation()
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint!))
.Build()
: null;
using var meterProvider = telemetryEnabled
? Sdk.CreateMeterProviderBuilder()
.SetResourceBuilder(resourceBuilder)
.AddMeter(ClawAgentFactory.OpenTelemetrySourceName)
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint!))
.Build()
: null;
if (!telemetryEnabled)
{
Console.WriteLine("Telemetry export is off. Set OTEL_EXPORTER_OTLP_ENDPOINT to send traces/metrics to an OTLP collector.");
}
using var meter = new Meter(ClawAgentFactory.OpenTelemetrySourceName);
var sessionCounter = meter.CreateCounter<int>("claw_console_sessions_total", description: "Interactive claw console sessions started.");
sessionCounter.Add(1);
await using ClawAgentBuild build = await ClawAgentFactory.CreateAsync(new ClawAgentFactoryOptions
{
Log = Console.WriteLine,
});
await HarnessConsole.RunAgentAsync(
build.Agent,
userPrompt: "Ask me to value a stock, score your portfolio risk, research some tickers, or tidy your trade confirmations.",
new HarnessConsoleOptions
{
Observers =
[
new OpenAIResponsesWebSearchDisplayObserver(),
new OpenAIResponsesErrorObserver(),
.. HarnessConsoleOptions.BuildObserversWithPlanning(
build.Agent,
planModeName: "plan",
executionModeName: "execute",
toolFormatters: ToolCallFormatter.BuildDefaultToolFormatters()),
],
CommandHandlers = HarnessConsoleOptions.BuildDefaultCommandHandlers(build.Agent),
});
@@ -0,0 +1,12 @@
# ClawAgent.Console
Interactive local host for the production-ready claw. It uses the shared `ClawAgentFactory` and the Step 03 console experience (`HarnessConsole.RunAgentAsync`) with planning observers and OpenAI Responses display helpers.
## Run
```bash
cd dotnet
dotnet run --project samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step04_ProductionReady/ClawAgent.Console
```
Set `OTEL_EXPORTER_OTLP_ENDPOINT` to send traces/metrics to an OTLP collector (for example a local Aspire dashboard). When it is not set, telemetry is not exported — there is no console exporter, because streaming spans and metrics to stdout would corrupt the interactive UI rendered by `HarnessConsole.RunAgentAsync`.
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);OPENAI001;MAAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ClawAgent\ClawAgent.csproj" />
<ProjectReference Include="..\..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.RegularExpressions;
using Azure.AI.Projects;
using Azure.Identity;
using ClawAgent;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI.Evaluation;
using FoundryEvals = Microsoft.Agents.AI.Foundry.FoundryEvals;
string[] queries =
[
"What's the capital of France?",
"Value MSFT for me.",
"How risky is my portfolio?",
];
await using ClawAgentBuild build = await ClawAgentFactory.CreateAsync(new ClawAgentFactoryOptions
{
// Evals run only the trusted skill scripts bundled with this sample. Auto-approve those scripts
// so evaluation receives the completed answer instead of an approval request.
AdditionalToolAutoApprovalRules = [AgentSkillsProvider.AllToolsAutoApprovalRule],
Log = Console.WriteLine,
});
Regex digitRegex = new(@"\d");
LocalEvaluator localEvaluator = new(
FunctionEvaluator.Create("off_topic_refusal_or_finance_steer", item =>
{
if (!item.Query.Contains("capital of France", StringComparison.OrdinalIgnoreCase))
{
return true;
}
return item.Response.Contains("finance", StringComparison.OrdinalIgnoreCase)
|| item.Response.Contains("invest", StringComparison.OrdinalIgnoreCase)
|| item.Response.Contains("portfolio", StringComparison.OrdinalIgnoreCase)
|| item.Response.Contains("outside", StringComparison.OrdinalIgnoreCase)
|| item.Response.Contains("can't", StringComparison.OrdinalIgnoreCase)
|| item.Response.Contains("cannot", StringComparison.OrdinalIgnoreCase);
}),
FunctionEvaluator.Create("numeric_valuation", item =>
!item.Query.Contains("Value MSFT", StringComparison.OrdinalIgnoreCase)
|| digitRegex.IsMatch(item.Response)),
FunctionEvaluator.Create("portfolio_risk_runs", item =>
!item.Query.Contains("portfolio", StringComparison.OrdinalIgnoreCase)
|| !string.IsNullOrWhiteSpace(item.Response)));
AgentEvaluationResults localResults = await build.Agent.EvaluateAsync(queries, localEvaluator, evalName: "ClawLocalFinanceEvals");
PrintResults("Local finance evals", localResults, queries);
string? endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT");
if (!string.IsNullOrWhiteSpace(endpoint))
{
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4";
AIProjectClient projectClient = new(new Uri(endpoint), new DefaultAzureCredential());
FoundryEvals foundryEvals = new(projectClient, deploymentName, FoundryEvals.Relevance, FoundryEvals.Coherence);
AgentEvaluationResults foundryResults = await build.Agent.EvaluateAsync(queries, foundryEvals, evalName: "ClawFoundryQualityEvals");
PrintResults("Foundry quality evals", foundryResults, queries);
}
else
{
Console.WriteLine("Skipping Foundry quality evals. Set FOUNDRY_PROJECT_ENDPOINT to enable them.");
}
static void PrintResults(string title, AgentEvaluationResults results, string[] queries)
{
Console.WriteLine($"=== {title} ===");
Console.WriteLine($"Provider: {results.ProviderName}");
Console.WriteLine($"Passed: {results.Passed}/{results.Total}");
if (results.ReportUrl is not null)
{
Console.WriteLine($"Report: {results.ReportUrl}");
}
Console.WriteLine();
for (int i = 0; i < results.Items.Count; i++)
{
Console.WriteLine($"Query: {(i < queries.Length ? queries[i] : "N/A")}");
Console.WriteLine($"Response: {(results.InputItems?[i].Response is { } response ? response[..Math.Min(80, response.Length)] : "N/A")}...");
foreach (var metric in results.Items[i].Metrics)
{
string value = metric.Value is NumericMetric numericMetric && numericMetric.Value.HasValue
? numericMetric.Value.Value.ToString("F1")
: metric.Value.Interpretation?.Failed == true ? "FAIL" : "PASS";
Console.WriteLine($" {metric.Key}: {value}");
}
Console.WriteLine();
}
}
@@ -0,0 +1,16 @@
# ClawAgent.Evals
Evaluation host for the production-ready claw.
It builds the shared agent with `ClawAgentFactory`, runs local finance checks with `LocalEvaluator` and `FunctionEvaluator.Create(...)`, and prints `Passed`/`Total`. When `FOUNDRY_PROJECT_ENDPOINT` is available, it also runs Foundry quality evals (`FoundryEvals.Relevance` and `FoundryEvals.Coherence`).
The eval host auto-approves only Agent Skills tools so the trusted scripts bundled with this sample
can produce complete answers. Trades, shell commands, file writes, and unrelated tools remain subject
to their normal approval behavior.
## Run
```bash
cd dotnet
dotnet run --project samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step04_ProductionReady/ClawAgent.Evals
```
@@ -0,0 +1,40 @@
# Files excluded from agent code deployment packaging.
# Uses .gitignore syntax.
# Note: only the root .agentignore is read; subdirectory files are not supported.
#
# To include a file that is excluded by default, use negation: !filename
# azd tooling files
agent.yaml
agent.manifest.yaml
azure.yaml
.agentignore
# Security / secrets
.env
.env.*
.azure/
.git/
# Python
__pycache__/
.venv/
venv/
*.pyc
*.pyo
.mypy_cache/
.pytest_cache/
# .NET
bin/
obj/
*.user
*.suo
.vs/
# Node
node_modules/
# Docker (not used in code deploy)
Dockerfile
.dockerignore
@@ -0,0 +1,10 @@
.env
bin/
obj/
.vs/
.vscode/
*.user
.azure/
.checkpoints/
agent-file-memory/
*.log
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<NoWarn>$(NoWarn);OPENAI001;MAAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNetEnv" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../ClawAgent/ClawAgent.csproj" />
<ProjectReference Include="../../../../../../src/Microsoft.Agents.AI.LocalCodeAct/Microsoft.Agents.AI.LocalCodeAct.csproj" />
<ProjectReference Include="../../../../../../src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="../../../../../../src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<ProjectReference Include="../../../../../04-hosting/FoundryHostedAgents/responses/Hosted_Shared_Contributor_Setup/Hosted_Shared_Contributor_Setup.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,34 @@
# Dockerfile for the ClawAgent.Hosted Foundry Hosted Agent, built from the agent-framework repo source.
#
# This project uses ProjectReference to local repo sources (ClawAgent, Microsoft.Agents.AI.Foundry,
# Microsoft.Agents.AI.Foundry.Hosting, Microsoft.Agents.AI.LocalCodeAct) and to the repo's Central
# Package Management, so a standard in-container `dotnet restore`/`publish` cannot resolve everything
# from this folder alone. Instead, PRE-PUBLISH the app on your machine (inside the full repo, where the
# references and package versions resolve) and COPY the output into the image:
#
# # 1. Build and publish separately, targeting the container runtime (glibc x64):
# dotnet publish -c Release -f net10.0 -r linux-x64 --self-contained false -o out
#
# # 2. Then build the image from the pre-published output:
# docker build -t personal-finance-claw .
#
# # 3. (Optional) run it locally:
# docker run --rm -p 8088:8088 --env-file .env personal-finance-claw
#
# `azd deploy` performs step 2 for you (building remotely in Azure Container Registry) — but you must
# still run step 1 (publish to ./out) first. See README.md.
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
# LocalCodeAct spawns Python to run and validate model-generated code, so the image needs python3.
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 \
&& rm -rf /var/lib/apt/lists/*
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENV LOCAL_CODEACT_PYTHON=python3
# The container is non-interactive: never block on a console prompt for a missing setting.
ENV AF_DEMO_NONINTERACTIVE=1
ENTRYPOINT ["dotnet", "ClawAgent.Hosted.dll"]
@@ -0,0 +1,91 @@
// Copyright (c) Microsoft. All rights reserved.
// Hosts the claw as a Foundry Hosted Agent (Responses API).
//
// Observability requires no extra wiring here: AddFoundryResponses automatically wraps the agent
// with OpenTelemetryAgent, and the Foundry hosting runtime (Azure.AI.AgentServer.Core's
// AddAgentHostTelemetry) registers the OTLP exporter pipeline. In the hosted environment Foundry
// injects APPLICATIONINSIGHTS_CONNECTION_STRING automatically, so traces, metrics and logs flow to
// Application Insights with no exporter configuration. To capture prompt/response content in traces,
// set OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true (off by default).
//
// File access and shell are DISABLED on the hosted agent. Granting the model arbitrary read/write
// access to the container filesystem, or letting it run shell commands, is a serious security risk in
// a shared hosted environment (data exfiltration, tampering, persistence) — and the local
// confirmations vault the shell operates on does not exist here. If you genuinely need file access
// when hosted, supply an external AgentFileStore (for example, one backed by Azure Blob Storage) via
// ClawAgentFactoryOptions.FileStore instead of using the container disk.
//
// CodeAct uses LocalCodeAct here, NOT the Hyperlight provider the local hosts use. Hyperlight runs
// guest code in a VM-isolated micro-sandbox that needs a hypervisor (KVM) and FUSE — neither of which
// an unprivileged Foundry hosted container exposes (attempting it fails at startup while configuring
// `fuse`, so the app never reports ready). LocalCodeAct instead runs the generated Python in a child
// process and relies on the hosted container itself as the isolation boundary, which is exactly the
// pattern the canonical Hosted-LocalCodeAct sample uses. SECURITY: LocalCodeAct is not itself a
// sandbox — only deploy it to an externally sandboxed environment such as a Foundry hosted-agent
// container.
using Azure.Core;
using Azure.Identity;
using ClawAgent;
using DotNetEnv;
using Hosted_Shared_Contributor_Setup;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Agents.AI.LocalCodeAct;
Env.TraversePath().Load();
var builder = WebApplication.CreateBuilder(args);
var httpContextAccessor = new HttpContextAccessor();
builder.Services.AddSingleton<IHttpContextAccessor>(httpContextAccessor);
var projectEndpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4";
var pythonExecutable = Environment.GetEnvironmentVariable("LOCAL_CODEACT_PYTHON") ?? "python3";
var purviewClientAppId = Environment.GetEnvironmentVariable("PURVIEW_CLIENT_APP_ID");
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in
// production. Prefer a specific credential (e.g. ManagedIdentityCredential) when hosted. Here we chain
// a temporary dev token (for local Docker debugging) ahead of DefaultAzureCredential (for local
// dotnet run / managed identity when hosted).
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());
await using ClawAgentBuild build = await ClawAgentFactory.CreateAsync(new ClawAgentFactoryOptions
{
ProjectEndpoint = projectEndpoint,
DeploymentName = deploymentName,
Credential = credential,
AgentDescription = "A production-ready personal finance claw with skills, CodeAct, background agents, telemetry, and optional Purview governance.",
PurviewCredential = string.IsNullOrWhiteSpace(purviewClientAppId) ? null : credential,
FoundryCallIdProvider = () =>
{
HttpContext? context = httpContextAccessor.HttpContext;
return context is null ? null : context.Request.Headers["x-agent-foundry-call-id"].ToString();
},
// Disable filesystem and shell access on the hosted container (see risk note above).
EnableFileAccess = false,
EnableShell = false,
// Use LocalCodeAct instead of the default Hyperlight provider: the hosted container has no
// hypervisor/FUSE for Hyperlight, and acts as the sandbox for the child Python process itself.
CodeActProvider = new LocalCodeActProvider(pythonExecutable),
Log = Console.WriteLine,
});
// AddFoundryResponses wires up the Responses API host for the agent and auto-applies OpenTelemetry.
builder.Services.AddFoundryResponses(build.Agent);
var app = builder.Build();
// Map the hosted-agent endpoint that live Foundry calls.
app.MapFoundryResponses();
// Contributor-only: map the per-agent OpenAI route shape for local debugging. Not used in production.
app.MapDevTemporaryLocalAgentEndpoint();
app.Run();
@@ -0,0 +1,186 @@
# ClawAgent.Hosted
ASP.NET host that serves the shared claw through the Foundry Responses hosting APIs.
The host is deliberately thin:
```csharp
var builder = WebApplication.CreateBuilder(args);
// Wires up the Responses API host for the agent and auto-applies OpenTelemetry.
builder.Services.AddFoundryResponses(build.Agent);
var app = builder.Build();
// The endpoint that live Foundry calls.
app.MapFoundryResponses();
// Contributor-only: local REPL route shape. Not used in production.
app.MapDevTemporaryLocalAgentEndpoint();
app.Run();
```
## Observability comes for free
No exporter wiring is required. `AddFoundryResponses` automatically wraps the agent with
`OpenTelemetryAgent`, and the Foundry hosting runtime (`Azure.AI.AgentServer.Core`'s
`AddAgentHostTelemetry`) registers the OTLP exporter pipeline. When hosted, Foundry injects
`APPLICATIONINSIGHTS_CONNECTION_STRING` automatically, so traces, metrics, and logs flow to
Application Insights with no configuration.
To capture prompt and response content in traces (off by default), set:
```bash
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
```
## File and shell access are disabled here
The hosted build turns **file access and shell off**:
```csharp
await using ClawAgentBuild build = await ClawAgentFactory.CreateAsync(new ClawAgentFactoryOptions
{
// ...
EnableFileAccess = false,
EnableShell = false,
});
```
Why: in a shared, hosted container, giving the model arbitrary read/write access to the filesystem, or
letting it run shell commands, is a serious security risk — data exfiltration, tampering, and
persistence — even behind a deny-list. The local confirmations vault the shell operates on doesn't
exist in the hosted environment anyway. If you enable either capability on a hosted container, treat it
as a production security decision and scope it tightly.
If you genuinely need file access when hosted, prefer supplying an **external `AgentFileStore`** (for
example, one backed by Azure Blob Storage) rather than the container disk:
```csharp
await using ClawAgentBuild build = await ClawAgentFactory.CreateAsync(new ClawAgentFactoryOptions
{
// ...
EnableFileAccess = true,
FileStore = new MyBlobAgentFileStore(blobContainerClient),
});
```
## CodeAct runs on LocalCodeAct here, not Hyperlight
The local hosts give the model a **Hyperlight**-backed CodeAct sandbox, which runs guest code in a
VM-isolated micro-sandbox. That needs a hypervisor (KVM) and FUSE — neither of which an unprivileged
Foundry hosted container exposes — so the Hyperlight provider can't initialize its sandbox when
hosted, and the agent never becomes ready.
The hosted build instead supplies a **`LocalCodeActProvider`**, which runs the generated Python in a
child process and relies on the hosted container itself as the isolation boundary:
```csharp
await using ClawAgentBuild build = await ClawAgentFactory.CreateAsync(new ClawAgentFactoryOptions
{
// ...
EnableFileAccess = false,
EnableShell = false,
// Hyperlight needs a hypervisor + FUSE the hosted container lacks; LocalCodeAct relies on the
// container as the sandbox. Override the interpreter with LOCAL_CODEACT_PYTHON if needed.
CodeActProvider = new LocalCodeActProvider(
Environment.GetEnvironmentVariable("LOCAL_CODEACT_PYTHON") ?? "python3"),
});
```
> **Security:** `LocalCodeAct` is not itself a sandbox — it executes model-generated Python in a child
> process. Only deploy it to an externally sandboxed environment such as a Foundry hosted-agent
> container. To turn CodeAct off entirely instead, set `EnableCodeAct = false`.
## Run locally
```bash
cd dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step04_ProductionReady/ClawAgent.Hosted
dotnet run
```
## Deploy to Foundry (container path)
This project deploys as a **container image** (not Foundry's source-code/zip path).
The project uses `ProjectReference` to sibling and
framework sources (`ClawAgent`, `Microsoft.Agents.AI.Foundry`, `.Foundry.Hosting`,
`.LocalCodeAct`) and the repo's Central Package Management (`dotnet/Directory.Packages.props`).
Because the ProjectReferences point outside this folder, a standard in-container `dotnet publish`
can't resolve them. So the flow is **two explicit steps**: publish locally first, then build/deploy
the image (this is what [`Dockerfile`](./Dockerfile) expects — it just
`COPY`s the pre-published `out/`).
**1. (First time only) initialize azd in container mode** (writes a `docker`-based `azure.yaml`):
```bash
azd ai agent init -m agent.manifest.yaml --deploy-mode container
```
`azd init` provisions (or reuses) a **container registry** and records it in the
`AZURE_CONTAINER_REGISTRY_ENDPOINT` environment variable, so you don't need to configure a registry
manually — azd pushes the built image there using this project's [`Dockerfile`](./Dockerfile)
automatically.
By default azd builds the image **remotely in Azure Container Registry**, so you don't need local
Docker. Set `remoteBuild: false` under the `docker:` options in `azure.yaml` to build locally
(requires Docker Desktop).
**2. Build and publish the app separately** (on your machine, inside the full repo, so the
ProjectReferences and package versions resolve). Target the container runtime (glibc x64):
```bash
cd dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step04_ProductionReady/ClawAgent.Hosted
dotnet publish -c Release -f net10.0 -r linux-x64 --self-contained false -o out
```
This produces `out/ClawAgent.Hosted.dll` and its dependencies. `out/` is what
`Dockerfile` copies — you must run this step **before** every image build/deploy.
**3. Grant the Foundry workspace identity `AcrPull` on the registry.** azd pushes the image, but the
hosted agent runtime pulls it using the Foundry **project's** system-assigned managed identity. That
identity needs `AcrPull` on your registry, or the deploy fails with *"Container registry
authentication failed … verify the workspace managed identity has AcrPull permissions"*:
```bash
# Get the project's system-assigned managed identity principal id:
PRINCIPAL_ID=$(az resource show \
--ids "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<foundry-account>/projects/<project-name>" \
--query identity.principalId -o tsv)
# Grant AcrPull on the registry:
az role assignment create \
--role AcrPull \
--assignee-principal-type ServicePrincipal \
--assignee-object-id "$PRINCIPAL_ID" \
--scope /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.ContainerRegistry/registries/<registry-name>
```
> RBAC changes can take a minute or two to propagate before the deploy can pull the image.
**4. Deploy:**
```bash
azd up # first deploy: provisions resources, builds the image, creates the agent version
# or, once provisioned (remember to re-run step 2 first so out/ is fresh):
azd deploy
```
**Test the image locally first (optional but recommended):**
```bash
# after step 2:
docker build -t personal-finance-claw .
docker run --rm -p 8088:8088 --env-file .env personal-finance-claw
# in another shell — should return HTTP 200:
curl -i http://localhost:8088/readiness
```
> **Non-interactive note:** the sample helpers prompt on the console for missing settings, which would
> block a non-interactive container. The image sets `AF_DEMO_NONINTERACTIVE=1` (and `az`-style hosts
> have redirected stdin) so startup never blocks. Provide real values via the `env:` map in
> `azure.yaml` or the container's environment. See the
> [container deployment guide](https://learn.microsoft.com/azure/foundry/agents/how-to/deploy-hosted-agent).
@@ -0,0 +1,38 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: personal-finance-claw
displayName: "Personal Finance Claw"
description: >
A production-ready personal finance claw hosted as a Foundry Hosted Agent with
Agent Framework harness capabilities, observability, optional Purview governance,
local finance skills, CodeAct, and background agents.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Agent Framework
- Observability
- Purview
- Evaluation
template:
name: personal-finance-claw
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "1.0"
memory: 2Gi
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
- name: TOOLBOX_MCP_SERVER_URL
value: "{{TOOLBOX_MCP_SERVER_URL}}"
- name: PURVIEW_CLIENT_APP_ID
value: "{{PURVIEW_CLIENT_APP_ID}}"
parameters:
properties: []
resources: []
@@ -0,0 +1,16 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: personal-finance-claw
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "1.0"
memory: 2Gi
environment_variables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
- name: TOOLBOX_MCP_SERVER_URL
value: ${TOOLBOX_MCP_SERVER_URL}
- name: PURVIEW_CLIENT_APP_ID
value: ${PURVIEW_CLIENT_APP_ID}
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);OPENAI001;MAAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Hyperlight.HyperlightSandbox.Guest.Python" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\..\src\Microsoft.Agents.AI.Harness\Microsoft.Agents.AI.Harness.csproj" />
<ProjectReference Include="..\..\..\..\..\..\src\Microsoft.Agents.AI.Hyperlight\Microsoft.Agents.AI.Hyperlight.csproj" />
<ProjectReference Include="..\..\..\..\..\..\src\Microsoft.Agents.AI.Mcp\Microsoft.Agents.AI.Mcp.csproj" />
<ProjectReference Include="..\..\..\..\..\..\src\Microsoft.Agents.AI.Purview\Microsoft.Agents.AI.Purview.csproj" />
<ProjectReference Include="..\..\..\..\..\..\src\Microsoft.Agents.AI.Tools.Shell\Microsoft.Agents.AI.Tools.Shell.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="skills\**\*" CopyToOutputDirectory="PreserveNewest" />
<Content Include="working\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
namespace ClawAgent;
/// <summary>
/// Contains the built production-ready claw agent and resources that must live as long as the agent.
/// </summary>
public sealed class ClawAgentBuild : IAsyncDisposable
{
private readonly List<IDisposable> _disposables;
private readonly List<IAsyncDisposable> _asyncDisposables;
private bool _disposed;
internal ClawAgentBuild(
AIAgent agent,
bool foundrySkillsEnabled,
bool purviewEnabled,
IEnumerable<IDisposable> disposables,
IEnumerable<IAsyncDisposable> asyncDisposables)
{
this.Agent = agent;
this.FoundrySkillsEnabled = foundrySkillsEnabled;
this.PurviewEnabled = purviewEnabled;
this._disposables = [.. disposables];
this._asyncDisposables = [.. asyncDisposables];
}
/// <summary>
/// Gets the fully configured claw agent.
/// </summary>
public AIAgent Agent { get; }
/// <summary>
/// Gets a value indicating whether Foundry Toolbox MCP skills were enabled.
/// </summary>
public bool FoundrySkillsEnabled { get; }
/// <summary>
/// Gets a value indicating whether Purview governance was enabled.
/// </summary>
public bool PurviewEnabled { get; }
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
if (this._disposed)
{
return;
}
this._disposed = true;
foreach (IAsyncDisposable disposable in this._asyncDisposables)
{
await disposable.DisposeAsync().ConfigureAwait(false);
}
foreach (IDisposable disposable in this._disposables)
{
disposable.Dispose();
}
}
}
@@ -0,0 +1,289 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ClientModel.Primitives;
using Azure.AI.Projects;
using Azure.Core;
using Azure.Identity;
using HyperlightSandbox.Guest.Python;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hyperlight;
using Microsoft.Agents.AI.Purview;
using Microsoft.Agents.AI.Tools.Shell;
using Microsoft.Extensions.AI;
namespace ClawAgent;
/// <summary>
/// Builds the shared production-ready claw agent used by all hosts.
/// </summary>
public static class ClawAgentFactory
{
/// <summary>
/// The OpenTelemetry source and meter name used by the claw harness agent.
/// </summary>
public const string OpenTelemetrySourceName = "BuildYourOwnClaw.ProductionReady.Claw";
private const string DefaultDeploymentName = "gpt-5.4";
private const string Instructions =
"""
## Personal Finance Assistant Instructions
You are a personal finance and investing assistant. You help the user understand their
portfolio and watchlist, value individual stocks, gauge portfolio risk, research the market,
and keep their records tidy.
### Working style
- The user's holdings live in a file called portfolio.csv. Read it with the file_access tools
before answering questions about their portfolio, and never modify it unless asked.
- You have skills for valuation and risk-scoring. When a question matches a skill, load it and
follow its instructions (read its references, run its scripts) rather than guessing.
- When asked to research several tickers, delegate each one to the background research agent so
they run concurrently, then summarize the findings together.
- The user's trade confirmations accumulate in the working/confirmations folder. When asked to
tidy or reorganize them, use the run_shell tool: inspect the folder first, then move files into
a year/month layout and rename them to YYYY-MM-DD_TICKER_BUY|SELL.txt. Explain your plan before
running commands that change anything.
- To buy or sell, use the place_trade tool. This takes a real action, so the user will be asked
to approve it before it runs explain what you are about to do first.
### Important
You provide information and analysis only you are not a licensed financial advisor and you
must not present your output as personalized investment advice. Remind the user to do their own
research before making decisions.
""";
/// <summary>
/// Creates the full claw agent and returns it with the resources that must be disposed by the host.
/// </summary>
/// <param name="options">Optional host-specific build settings.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The built agent and disposable resources.</returns>
public static async Task<ClawAgentBuild> CreateAsync(ClawAgentFactoryOptions? options = null, CancellationToken cancellationToken = default)
{
options ??= new ClawAgentFactoryOptions();
Action<string> log = options.Log ?? Console.WriteLine;
string endpoint = options.ProjectEndpoint
?? Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = options.DeploymentName
?? Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME")
?? DefaultDeploymentName;
string workingDir = options.WorkingDirectory ?? Path.Combine(AppContext.BaseDirectory, "working");
string vaultDir = Path.Combine(workingDir, "confirmations");
string skillsDir = options.SkillsDirectory ?? Path.Combine(AppContext.BaseDirectory, "skills");
TokenCredential credential = options.Credential ?? new DefaultAzureCredential();
AIProjectClient projectClient = new(
new Uri(endpoint),
credential,
new AIProjectClientOptions { RetryPolicy = new ClientRetryPolicy(3) });
IChatClient chatClient = projectClient
.GetProjectOpenAIClient()
.GetResponsesClient()
.AsIChatClient(deploymentName);
bool purviewEnabled = false;
string? purviewClientAppId = Environment.GetEnvironmentVariable("PURVIEW_CLIENT_APP_ID");
TokenCredential? purviewCredential = options.PurviewCredential;
if (purviewCredential is null && !string.IsNullOrWhiteSpace(purviewClientAppId))
{
purviewCredential = new InteractiveBrowserCredential(
new InteractiveBrowserCredentialOptions { ClientId = purviewClientAppId });
}
if (purviewCredential is not null)
{
chatClient = chatClient
.AsBuilder()
.WithPurview(purviewCredential, new PurviewSettings("Claw"))
.Build();
purviewEnabled = true;
log(options.PurviewCredential is not null
? "Purview enabled (host-provided credential). "
: "Purview enabled (interactive browser credential). ");
}
else
{
log("Purview disabled. Set PURVIEW_CLIENT_APP_ID to enable governance checks.");
}
var skillsBuilder = new AgentSkillsProviderBuilder()
.UseFileSkills([skillsDir], scriptRunner: new SubprocessScriptRunner().RunAsync);
HttpClient? toolboxHttpClient = null;
ModelContextProtocol.Client.McpClient? toolboxMcpClient = null;
string? toolboxUrl = Environment.GetEnvironmentVariable("TOOLBOX_MCP_SERVER_URL");
bool foundrySkillsEnabled = false;
if (!string.IsNullOrWhiteSpace(toolboxUrl))
{
(toolboxMcpClient, toolboxHttpClient) = await FoundrySkills.ConnectAsync(
toolboxUrl,
credential,
options.FoundryCallIdProvider,
cancellationToken).ConfigureAwait(false);
skillsBuilder.UseMcpSkills(toolboxMcpClient);
foundrySkillsEnabled = true;
log("Foundry skills enabled (Toolbox MCP). ");
}
else
{
log("Foundry skills disabled. Set TOOLBOX_MCP_SERVER_URL to enable them.");
}
skillsBuilder.UseOptions((options) =>
{
options.DisableLoadSkillApproval = true;
options.DisableReadSkillResourceApproval = true;
});
AgentSkillsProvider skillsProvider = skillsBuilder.Build();
AIAgent researchAgent = ResearchAgent.Create(chatClient);
// Shell access is a powerful capability. It is confined to the local vault directory with a
// deny-list here, but on shared/hosted deployments it is disabled entirely (see hosted host).
LocalShellExecutor? shell = null;
if (options.EnableShell)
{
shell = new LocalShellExecutor(new LocalShellExecutorOptions
{
WorkingDirectory = vaultDir,
ConfineWorkingDirectory = true,
Policy = new ShellPolicy(denyList:
[
@"\brm\s+-rf\b",
@"\bsudo\b",
@":\(\)\s*\{",
@"\bmkfs\b",
@">\s*/dev/sd",
]),
Timeout = TimeSpan.FromSeconds(15),
});
log("Shell enabled (confined to the confirmations vault). ");
}
else
{
log("Shell disabled. ");
}
// File access is enabled by default via a filesystem-backed store. Hosts may disable it or
// supply an external store (for example, backed by blob storage) instead of the container disk.
AgentFileStore? fileStore = null;
if (options.EnableFileAccess)
{
fileStore = options.FileStore ?? new FileSystemAgentFileStore(workingDir);
log(options.FileStore is not null
? "File access enabled (custom AgentFileStore). "
: "File access enabled (local filesystem). ");
}
else
{
log("File access disabled. ");
}
// CodeAct gives the model a sandboxed code interpreter. By default we use the Hyperlight
// provider, which runs guest code in a VM-isolated micro-sandbox — great for local hosts, but
// it needs a hypervisor (KVM) and FUSE, which an unprivileged Foundry hosted container does not
// expose. Hosts running in such an environment supply their own provider via
// options.CodeActProvider (for example a LocalCodeActProvider that relies on the container
// itself as the isolation boundary) or disable CodeAct entirely with EnableCodeAct = false.
AIContextProvider? codeAct = null;
if (options.EnableCodeAct)
{
codeAct = options.CodeActProvider
?? new HyperlightCodeActProvider(HyperlightCodeActProviderOptions.CreateForWasm(PythonGuestModule.GetModulePath()));
log(options.CodeActProvider is not null
? "CodeAct enabled (custom provider). "
: "CodeAct enabled (Hyperlight VM-isolated sandbox). ");
}
else
{
log("CodeAct disabled. ");
}
List<AIContextProvider> contextProviders = [skillsProvider];
if (codeAct is not null)
{
contextProviders.Add(codeAct);
}
List<AITool> tools =
[
StockTools.CreateGetStockPriceTool(),
TradingTools.CreatePlaceTradeTool(),
];
// Shell support is composed explicitly: the context provider tells the model about the
// environment, while the approval-gated function exposes command execution.
if (shell is not null)
{
contextProviders.Add(new ShellEnvironmentProvider(shell));
tools.Add(shell.AsAIFunction(requireApproval: true));
}
List<Func<ToolAutoApprovalRuleContext, ValueTask<bool>>> autoApprovalRules =
[
FileAccessProvider.ReadOnlyToolsAutoApprovalRule,
];
if (options.AdditionalToolAutoApprovalRules is not null)
{
autoApprovalRules.AddRange(options.AdditionalToolAutoApprovalRules);
}
AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
Name = options.AgentName,
Description = options.AgentDescription,
FileAccessStore = fileStore,
DisableAgentSkillsProvider = true,
BackgroundAgents = [researchAgent],
OpenTelemetrySourceName = OpenTelemetrySourceName,
ToolApprovalAgentOptions = new ToolApprovalAgentOptions
{
AutoApprovalRules = autoApprovalRules,
},
AgentModeProviderOptions = new AgentModeProviderOptions { DefaultMode = "execute" },
AIContextProviders = contextProviders,
ChatOptions = new ChatOptions
{
Instructions = Instructions,
Tools = tools,
Reasoning = new() { Effort = ReasoningEffort.Medium },
},
});
List<IDisposable> disposables = [];
if (codeAct is IDisposable disposableCodeAct)
{
disposables.Add(disposableCodeAct);
}
if (toolboxHttpClient is not null)
{
disposables.Add(toolboxHttpClient);
}
if (chatClient is IDisposable disposableChatClient)
{
disposables.Add(disposableChatClient);
}
List<IAsyncDisposable> asyncDisposables = [];
if (shell is not null)
{
asyncDisposables.Add(shell);
}
if (toolboxMcpClient is not null)
{
asyncDisposables.Add(toolboxMcpClient);
}
return new ClawAgentBuild(agent, foundrySkillsEnabled, purviewEnabled, disposables, asyncDisposables);
}
}
@@ -0,0 +1,133 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.Core;
using Microsoft.Agents.AI;
namespace ClawAgent;
/// <summary>
/// Options for building the production-ready claw agent.
/// </summary>
public sealed class ClawAgentFactoryOptions
{
/// <summary>
/// Gets or sets the Foundry project endpoint. Defaults to <c>FOUNDRY_PROJECT_ENDPOINT</c>.
/// </summary>
public string? ProjectEndpoint { get; set; }
/// <summary>
/// Gets or sets the Foundry model deployment name. Defaults to <c>AZURE_AI_MODEL_DEPLOYMENT_NAME</c> or <c>gpt-5.4</c>.
/// </summary>
public string? DeploymentName { get; set; }
/// <summary>
/// Gets or sets the token credential used for Foundry. Defaults to <see cref="Azure.Identity.DefaultAzureCredential" />.
/// </summary>
public TokenCredential? Credential { get; set; }
/// <summary>
/// Gets or sets the token credential used for Purview. When provided, Purview is enabled with
/// this credential. Otherwise, <c>PURVIEW_CLIENT_APP_ID</c> enables local browser authentication.
/// </summary>
public TokenCredential? PurviewCredential { get; set; }
/// <summary>
/// Gets or sets an optional provider for the current Foundry hosted call ID. When supplied, the
/// call ID is forwarded to the Toolbox MCP endpoint as <c>x-agent-foundry-call-id</c>.
/// </summary>
public Func<string?>? FoundryCallIdProvider { get; set; }
/// <summary>
/// Gets or sets the agent name exposed to hosting and telemetry.
/// </summary>
public string AgentName { get; set; } = "personal-finance-claw";
/// <summary>
/// Gets or sets the agent description exposed to hosting and telemetry.
/// </summary>
public string AgentDescription { get; set; } = "A production-ready personal finance claw with skills, shell, CodeAct, background agents, telemetry, and optional Purview governance.";
/// <summary>
/// Gets or sets the working directory containing portfolio data and trade confirmations.
/// </summary>
public string? WorkingDirectory { get; set; }
/// <summary>
/// Gets or sets the directory containing file-based skills.
/// </summary>
public string? SkillsDirectory { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the agent can read and write files on the host.
/// </summary>
/// <remarks>
/// Enabled by default for local hosts. Disable it on shared/hosted deployments where giving the
/// model arbitrary read/write access to the container filesystem is a data-exfiltration and
/// tampering risk. When you still need file access in a hosted environment, prefer supplying an
/// external <see cref="FileStore"/> (for example, a blob-storage-backed store) rather than the
/// container disk.
/// </remarks>
public bool EnableFileAccess { get; set; } = true;
/// <summary>
/// Gets or sets an optional custom <see cref="AgentFileStore"/> used for file access.
/// </summary>
/// <remarks>
/// When <see langword="null"/> (and <see cref="EnableFileAccess"/> is <see langword="true"/>), a
/// <see cref="FileSystemAgentFileStore"/> rooted at <see cref="WorkingDirectory"/> is used. Supply
/// your own store to keep files off the local disk — for example, a store backed by Azure Blob
/// Storage — which is the recommended approach for hosted deployments. Ignored when
/// <see cref="EnableFileAccess"/> is <see langword="false"/>.
/// </remarks>
public AgentFileStore? FileStore { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the agent can run shell commands on the host.
/// </summary>
/// <remarks>
/// Enabled by default for local hosts. Disable it on shared/hosted deployments: arbitrary command
/// execution inside the hosted container is a serious security risk (data exfiltration, persistence,
/// tampering) even with a deny-list, and the local vault it operates on does not exist in the
/// hosted environment.
/// </remarks>
public bool EnableShell { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether the agent exposes a CodeAct code interpreter.
/// </summary>
/// <remarks>
/// Enabled by default. When <see langword="true"/> and <see cref="CodeActProvider"/> is
/// <see langword="null"/>, a Hyperlight-backed, VM-isolated provider is used — suitable for local
/// hosts with a hypervisor (KVM) and FUSE. Foundry hosted containers do not expose those, so a
/// hosted host should either supply a <see cref="CodeActProvider"/> that relies on the container as
/// the sandbox (for example a <c>LocalCodeActProvider</c>) or set this to <see langword="false"/>.
/// </remarks>
public bool EnableCodeAct { get; set; } = true;
/// <summary>
/// Gets or sets an optional CodeAct context provider used when <see cref="EnableCodeAct"/> is
/// <see langword="true"/>.
/// </summary>
/// <remarks>
/// When <see langword="null"/> the factory creates a Hyperlight-backed provider. Supply your own
/// (for example a <c>LocalCodeActProvider</c>) to run in an environment without a hypervisor, such
/// as a Foundry hosted container. Ignored when <see cref="EnableCodeAct"/> is
/// <see langword="false"/>. If the provider implements <see cref="IDisposable"/> it is
/// disposed by the returned <see cref="ClawAgentBuild"/>.
/// </remarks>
public AIContextProvider? CodeActProvider { get; set; }
/// <summary>
/// Gets or sets additional tool auto-approval rules for this host.
/// </summary>
/// <remarks>
/// The factory always includes the read-only file-access rule. Use additional rules only in
/// trusted hosts, such as an evaluation runner that executes bundled skill scripts.
/// </remarks>
public IEnumerable<Func<ToolAutoApprovalRuleContext, ValueTask<bool>>>? AdditionalToolAutoApprovalRules { get; set; }
/// <summary>
/// Gets or sets the optional log callback used for setup notes.
/// </summary>
public Action<string>? Log { get; set; }
}
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net.Http.Headers;
using Azure.Core;
using ModelContextProtocol.Client;
namespace ClawAgent;
/// <summary>
/// Helpers for wiring centrally-managed Foundry skills into the claw via a Foundry Toolbox MCP endpoint.
/// </summary>
internal static class FoundrySkills
{
/// <summary>
/// Connects to a Foundry Toolbox MCP endpoint and returns a connected MCP client.
/// </summary>
public static async Task<(McpClient McpClient, HttpClient HttpClient)> ConnectAsync(
string toolboxMcpServerUrl,
TokenCredential credential,
Func<string?>? foundryCallIdProvider = null,
CancellationToken cancellationToken = default)
{
var httpClient = new HttpClient(new BearerTokenHandler(credential, "https://ai.azure.com/.default", foundryCallIdProvider)
{
InnerHandler = new HttpClientHandler(),
});
try
{
McpClient mcpClient = await McpClient.CreateAsync(
new HttpClientTransport(
new HttpClientTransportOptions
{
Endpoint = new Uri(toolboxMcpServerUrl),
Name = "foundry_toolbox",
TransportMode = HttpTransportMode.StreamableHttp,
AdditionalHeaders = new Dictionary<string, string>
{
["Foundry-Features"] = "Toolboxes=V1Preview",
},
},
httpClient),
cancellationToken: cancellationToken).ConfigureAwait(false);
return (mcpClient, httpClient);
}
catch
{
httpClient.Dispose();
throw;
}
}
private sealed class BearerTokenHandler(
TokenCredential credential,
string scope,
Func<string?>? foundryCallIdProvider) : DelegatingHandler
{
private readonly TokenRequestContext _tokenContext = new([scope]);
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
AccessToken token = await credential.GetTokenAsync(this._tokenContext, cancellationToken).ConfigureAwait(false);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
string? callId = foundryCallIdProvider?.Invoke();
if (!string.IsNullOrWhiteSpace(callId) && !request.Headers.Contains("x-agent-foundry-call-id"))
{
request.Headers.TryAddWithoutValidation("x-agent-foundry-call-id", callId);
}
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,7 @@
# ClawAgent
Shared class library for the production-ready personal finance claw.
`ClawAgentFactory.CreateAsync(...)` returns a `ClawAgentBuild` containing the fully configured `AIAgent` plus disposable resources. It preserves the Step 03 capabilities: Foundry Responses `IChatClient`, local file skills, optional Foundry Toolbox MCP skills, background research agent, confined `LocalShellExecutor`, Hyperlight CodeAct, file access, approvals, agent modes, stock tools, and trading tools.
Purview is opt-in via `PURVIEW_CLIENT_APP_ID`; when unset, the chat client is not wrapped. Telemetry is always enabled through `HarnessAgentOptions.OpenTelemetrySourceName`.
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace ClawAgent;
/// <summary>
/// Builds the background research agent that the main claw fans work out to.
/// </summary>
internal static class ResearchAgent
{
/// <summary>
/// Creates a web-search-only background agent for delegated ticker research.
/// </summary>
public static AIAgent Create(IChatClient chatClient) =>
chatClient.AsAIAgent(
instructions:
"You research a single stock ticker. Use the web search tool to find the most " +
"recent, relevant news and commentary, then return a short, factual summary " +
"(3-4 bullet points) with no preamble.",
name: "TickerResearchAgent",
description: "Searches the web for recent news and commentary about a single stock ticker.",
tools: [new HostedWebSearchTool()]);
}
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.Extensions.AI;
namespace ClawAgent;
/// <summary>
/// A custom function tool that gives the claw access to illustrative stock prices.
/// </summary>
internal static class StockTools
{
/// <summary>
/// A delayed, illustrative stock quote, including trailing earnings-per-share.
/// </summary>
public sealed record StockQuote(string Symbol, decimal Price, decimal TrailingEps, string Currency, DateTimeOffset AsOf);
private static readonly Dictionary<string, (decimal Price, decimal Eps)> s_priceBook = new(StringComparer.OrdinalIgnoreCase)
{
["MSFT"] = (462.97m, 11.80m),
["AAPL"] = (229.35m, 6.13m),
["GOOGL"] = (178.12m, 7.54m),
["AMZN"] = (201.45m, 4.18m),
["NVDA"] = (134.81m, 2.95m),
["SPY"] = (612.40m, 23.10m),
};
/// <summary>
/// Gets the latest delayed, illustrative stock price and trailing EPS for a ticker symbol.
/// </summary>
[Description("Gets the latest (delayed, illustrative) stock price and trailing earnings per share for a ticker symbol.")]
public static StockQuote GetStockPrice(
[Description("The stock ticker symbol, e.g. MSFT or AAPL.")] string symbol)
{
if (!s_priceBook.TryGetValue(symbol, out var data))
{
var seed = 0;
foreach (var ch in symbol.ToUpperInvariant())
{
seed = (seed * 31 + ch) % 1_000_000;
}
var price = 50m + seed % 45000 / 100m;
data = (price, Math.Round(price / 20m, 2));
}
return new StockQuote(symbol.ToUpperInvariant(), data.Price, data.Eps, "USD", DateTimeOffset.UtcNow);
}
/// <summary>
/// Creates the AI function wrapper used to expose the stock price tool to the agent.
/// </summary>
public static AIFunction CreateGetStockPriceTool() => AIFunctionFactory.Create(GetStockPrice, "get_stock_price");
}
@@ -0,0 +1,179 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace ClawAgent;
/// <summary>
/// Executes file-based skill scripts as local subprocesses.
/// </summary>
internal sealed class SubprocessScriptRunner
{
private static readonly TimeSpan s_scriptTimeout = TimeSpan.FromSeconds(30);
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="SubprocessScriptRunner" /> class.
/// </summary>
public SubprocessScriptRunner(ILoggerFactory? loggerFactory = null)
{
this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<SubprocessScriptRunner>();
}
/// <summary>
/// Runs a skill script as a local subprocess.
/// </summary>
public async Task<object?> RunAsync(
AgentFileSkill skill,
AgentFileSkillScript script,
JsonElement? arguments,
IServiceProvider? serviceProvider,
CancellationToken cancellationToken)
{
this._logger.LogDebug("Running script '{ScriptName}' from skill '{SkillName}'.", script.Name, skill.Frontmatter.Name);
if (!File.Exists(script.FullPath))
{
this._logger.LogError("Script file not found for skill '{SkillName}': {ScriptPath}", skill.Frontmatter.Name, script.FullPath);
return $"Error: Script file not found: {script.FullPath}";
}
string extension = Path.GetExtension(script.FullPath);
string? interpreter = extension switch
{
".py" => OperatingSystem.IsWindows() ? "python" : "python3",
".js" => "node",
".sh" => "bash",
".ps1" => "pwsh",
_ => null,
};
var startInfo = new ProcessStartInfo
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = Path.GetDirectoryName(script.FullPath) ?? ".",
};
if (interpreter is not null)
{
startInfo.FileName = interpreter;
startInfo.ArgumentList.Add(script.FullPath);
}
else
{
startInfo.FileName = script.FullPath;
}
if (arguments is { ValueKind: JsonValueKind.Array } json)
{
foreach (var element in json.EnumerateArray())
{
if (element.ValueKind != JsonValueKind.String)
{
throw new InvalidOperationException(
$"File-based skill scripts only accept string CLI arguments but received a JSON element of kind '{element.ValueKind}'. " +
"All array elements must be JSON strings.");
}
startInfo.ArgumentList.Add(element.GetString()!);
}
}
else if (arguments?.ValueKind is not null and not JsonValueKind.Null and not JsonValueKind.Undefined)
{
throw new InvalidOperationException(
$"Expected a JSON array of CLI arguments but received {arguments.Value.ValueKind}. " +
"File-based skill scripts expect positional arguments as a JSON array of strings.");
}
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(s_scriptTimeout);
CancellationToken runToken = timeoutCts.Token;
Process? process = null;
try
{
process = Process.Start(startInfo);
if (process is null)
{
this._logger.LogError("Failed to start process for script '{ScriptName}' from skill '{SkillName}'.", script.Name, skill.Frontmatter.Name);
return $"Error: Failed to start process for script '{script.Name}'.";
}
Task<string> outputTask = process.StandardOutput.ReadToEndAsync(runToken);
Task<string> errorTask = process.StandardError.ReadToEndAsync(runToken);
await process.WaitForExitAsync(runToken).ConfigureAwait(false);
string output = await outputTask.ConfigureAwait(false);
string error = await errorTask.ConfigureAwait(false);
if (!string.IsNullOrEmpty(error))
{
if (process.ExitCode == 0)
{
this._logger.LogWarning(
"Script '{ScriptName}' from skill '{SkillName}' succeeded but wrote to stderr:\n{Stderr}",
script.Name, skill.Frontmatter.Name, error.Trim());
}
output += $"\nStderr:\n{error}";
}
if (process.ExitCode != 0)
{
this._logger.LogError(
"Script '{ScriptName}' from skill '{SkillName}' exited with code {ExitCode}.{Stderr}",
script.Name,
skill.Frontmatter.Name,
process.ExitCode,
string.IsNullOrEmpty(error) ? string.Empty : $"\nStderr:\n{error.Trim()}");
output += $"\nScript exited with code {process.ExitCode}";
}
string result = string.IsNullOrEmpty(output) ? "(no output)" : output.Trim();
if (process.ExitCode == 0)
{
this._logger.LogInformation(
"Script '{ScriptName}' from skill '{SkillName}' completed successfully. Output:\n{Output}",
script.Name,
skill.Frontmatter.Name,
result);
}
return result;
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
process?.Kill(entireProcessTree: true);
this._logger.LogError(
"Script '{ScriptName}' from skill '{SkillName}' timed out after {Timeout} seconds.",
script.Name,
skill.Frontmatter.Name,
s_scriptTimeout.TotalSeconds);
return $"Error: Script '{script.Name}' timed out after {s_scriptTimeout.TotalSeconds:0} seconds.";
}
catch (OperationCanceledException)
{
process?.Kill(entireProcessTree: true);
throw;
}
catch (Exception ex)
{
this._logger.LogError(ex, "Failed to execute script '{ScriptName}' from skill '{SkillName}'.", script.Name, skill.Frontmatter.Name);
return $"Error: Failed to execute script '{script.Name}': {ex.Message}";
}
finally
{
process?.Dispose();
}
}
}
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.Extensions.AI;
namespace ClawAgent;
/// <summary>
/// Sensitive claw tools that take real-world actions and therefore require human approval.
/// </summary>
internal static class TradingTools
{
/// <summary>
/// Places a simulated buy or sell order for a given symbol and quantity.
/// </summary>
[Description("Places a buy or sell order for a given symbol and quantity.")]
public static string PlaceTrade(
[Description("The stock ticker symbol to trade, e.g. MSFT.")] string symbol,
[Description("Either 'buy' or 'sell'.")] string action,
[Description("The number of shares to trade.")] int quantity)
{
var isBuy = action.Equals("buy", StringComparison.OrdinalIgnoreCase);
var isSell = action.Equals("sell", StringComparison.OrdinalIgnoreCase);
if (!isBuy && !isSell)
{
return $"Invalid action '{action}'. Use 'buy' or 'sell'.";
}
if (quantity <= 0)
{
return $"Invalid quantity '{quantity}'. Quantity must be a positive whole number of shares.";
}
var verb = isSell ? "Sold" : "Bought";
var confirmation = $"TRADE-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
return $"{verb} {quantity} share(s) of {symbol.ToUpperInvariant()}. Confirmation: {confirmation}.";
}
/// <summary>
/// Creates an approval-required AI function for placing trades.
/// </summary>
public static AIFunction CreatePlaceTradeTool() =>
new ApprovalRequiredAIFunction(AIFunctionFactory.Create(PlaceTrade, "place_trade"));
}
@@ -0,0 +1,18 @@
---
name: risk-scoring
description: Score how concentrated and risky a portfolio is on a 0-100 scale from its position weights. Use when the user asks how risky their portfolio is, whether it is too concentrated, or for a diversification check.
---
## Usage
When the user asks about portfolio risk or concentration:
1. Read `references/risk-bands.md` to understand the score bands and what drives them.
2. Compute each holding's market value (shares × price) — use the `get_stock_price` tool for current
prices if you do not already have them.
3. Run `scripts/risk_score.py` with one `--position VALUE` argument per holding,
e.g. `--position 18518 --position 17201 --position 16177`.
4. Report the 0-100 score, the band it falls in, and the largest single-position weight, then suggest
(in general terms) whether the portfolio looks well diversified or concentrated.
Remind the user this is a crude concentration measure, not a complete risk model, and not advice.
@@ -0,0 +1,27 @@
# Risk-scoring guide (illustrative)
This skill scores **concentration risk** — how much a portfolio depends on its largest positions —
on a 0-100 scale, where higher means riskier.
## How the score is built
1. Convert each position to a weight: `weight = position_value / total_value`.
2. Compute the Herfindahl-Hirschman Index (HHI): `HHI = sum(weight^2)`.
- A perfectly even portfolio of *n* holdings has `HHI = 1/n` (low).
- A single-stock portfolio has `HHI = 1` (maximum concentration).
3. Scale to 0-100: `score = round(HHI * 100)`.
## Score bands
| Score | Band | Interpretation |
|---------|--------------------|-------------------------------------------------|
| 0-20 | Well diversified | No single holding dominates. |
| 21-40 | Moderately diversified | Some tilt, but broadly spread. |
| 41-60 | Concentrated | A few positions carry most of the risk. |
| 61-100 | Highly concentrated| Heavily dependent on one or two positions. |
Also watch the **largest single-position weight**: above ~25% is usually worth flagging regardless
of the overall score.
This measures concentration only — it ignores volatility, correlation, sector exposure, and leverage,
so it is a starting point, not a verdict.
@@ -0,0 +1,58 @@
# Portfolio risk-scoring script
# Scores concentration risk on a 0-100 scale using the Herfindahl-Hirschman Index (HHI).
#
# weight_i = position_i / total
# HHI = sum(weight_i ^ 2)
# score = round(HHI * 100) # higher = more concentrated = riskier
#
# Usage:
# python scripts/risk_score.py --position 18518 --position 17201 --position 16177
import argparse
import json
def main() -> None:
parser = argparse.ArgumentParser(description="Score portfolio concentration risk (0-100).")
parser.add_argument(
"--position",
type=float,
action="append",
required=True,
help="Market value of one holding. Pass once per position.",
)
args = parser.parse_args()
positions = args.position
if any(p <= 0 for p in positions):
print(json.dumps({"error": "Each position value must be a positive market value."}))
return
total = sum(positions)
if total <= 0:
print(json.dumps({"error": "Total portfolio value must be positive."}))
return
weights = [p / total for p in positions]
hhi = sum(w * w for w in weights)
score = round(hhi * 100)
if score <= 20:
band = "Well diversified"
elif score <= 40:
band = "Moderately diversified"
elif score <= 60:
band = "Concentrated"
else:
band = "Highly concentrated"
print(json.dumps({
"positions": len(positions),
"score": score,
"band": band,
"largest_weight_pct": round(max(weights) * 100, 1),
}))
if __name__ == "__main__":
main()
@@ -0,0 +1,17 @@
---
name: valuation
description: Estimate whether a stock looks cheap or expensive using a price-to-earnings (P/E) based fair-value method. Use when the user asks if a stock is over- or under-valued, or for a fair-value / target price.
---
## Usage
When the user asks whether a stock is fairly valued, over-valued, or under-valued:
1. Read `references/valuation-guide.md` to pick a sensible target P/E for the company's sector.
2. Run `scripts/valuation_metrics.py` with the current price, trailing EPS, and the target P/E,
e.g. `--price 462.97 --eps 11.80 --target-pe 32`.
3. Report the computed P/E, the fair-value estimate, and the percentage upside/downside, then state
plainly whether the stock looks cheap or expensive on this measure.
Always remind the user that a single P/E heuristic is not investment advice and ignores growth,
debt, and many other factors.
@@ -0,0 +1,28 @@
# Valuation guide (illustrative)
A quick price-to-earnings (P/E) sanity check:
- **P/E = price ÷ trailing earnings per share (EPS)**
- **Fair value = trailing EPS × target P/E**
- **Upside/downside = (fair value price) ÷ price**
## Typical target P/E by sector
These are rough, illustrative anchors only — not live market multiples.
| Sector | Conservative target P/E | Growth target P/E |
|-----------------------|-------------------------|-------------------|
| Mega-cap technology | 28 | 35 |
| Semiconductors | 25 | 40 |
| Consumer staples | 18 | 22 |
| Financials / banks | 11 | 14 |
| Broad market (index) | 19 | 21 |
## How to read the result
- Fair value **well above** the current price ⇒ the stock looks **cheap** on this measure.
- Fair value **well below** the current price ⇒ the stock looks **expensive** on this measure.
- Within ~5% ⇒ roughly **fairly valued**.
This is one crude lens. It ignores growth rates, balance-sheet strength, and cash flow, so never
present it as a recommendation.
@@ -0,0 +1,57 @@
# Valuation metrics script
# Computes a simple price-to-earnings (P/E) based fair-value estimate.
#
# fair_value = eps * target_pe
# pe = price / eps
# upside = (fair_value - price) / price
#
# Usage:
# python scripts/valuation_metrics.py --price 462.97 --eps 11.80 --target-pe 32
import argparse
import json
def main() -> None:
parser = argparse.ArgumentParser(description="Compute a P/E based fair-value estimate.")
parser.add_argument("--price", type=float, required=True, help="Current share price.")
parser.add_argument("--eps", type=float, required=True, help="Trailing earnings per share.")
parser.add_argument("--target-pe", type=float, required=True, help="Target P/E from the guide.")
args = parser.parse_args()
if args.eps <= 0:
print(json.dumps({"error": "EPS must be positive to compute a P/E ratio."}))
return
if args.price <= 0:
print(json.dumps({"error": "Price must be positive to compute valuation metrics."}))
return
if args.target_pe <= 0:
print(json.dumps({"error": "Target P/E must be positive."}))
return
pe = args.price / args.eps
fair_value = args.eps * args.target_pe
upside = (fair_value - args.price) / args.price
if upside > 0.05:
verdict = "looks cheap"
elif upside < -0.05:
verdict = "looks expensive"
else:
verdict = "roughly fairly valued"
print(json.dumps({
"price": round(args.price, 2),
"eps": round(args.eps, 2),
"target_pe": round(args.target_pe, 2),
"pe": round(pe, 2),
"fair_value": round(fair_value, 2),
"upside_pct": round(upside * 100, 1),
"verdict": verdict,
}))
if __name__ == "__main__":
main()
@@ -0,0 +1,6 @@
TRADE CONFIRMATION
Confirmation: TRADE-55AA44BB
Date: 2025-06-21
Symbol: NVDA
Action: SELL
Quantity: 20
@@ -0,0 +1,6 @@
TRADE CONFIRMATION
Confirmation: TRADE-77CC88DD
Date: 2024-05-08
Symbol: SPY
Action: SELL
Quantity: 15
@@ -0,0 +1,6 @@
TRADE CONFIRMATION
Confirmation: TRADE-9F8E7D6C
Date: 2024-11-03
Symbol: AAPL
Action: BUY
Quantity: 75

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