Compare commits

...

55 Commits

Author SHA1 Message Date
dependabot[bot] 7433aab92b Bump github/codeql-action/analyze from 4.37.3 to 4.37.7
Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.37.3 to 4.37.7.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81...ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-20 10:18:58 +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
Chinedum Echeta 9c3a1a4af7 Python: Enhance _OutputItemTracker to prevent duplicate function call streaming (#7486)
* Python: Enhance _OutputItemTracker to prevent duplicate function call streaming

* Handle empty function call metadata arguments

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

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

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

---------

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

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

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

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

* Apply new assignments after feedback

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

* Address PR comments

* Update spec

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

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

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

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

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

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

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

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

* Enforce one pending functional continuation

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

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

Next iteration: preserve and document authorized checkpoint continuation boundaries.

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

* Preserve authorized functional checkpoint continuation

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

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

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

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

* Validate Python continuation hardening

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

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

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

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

* Handle functional checkpoint continuation failures

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

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

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

* Address functional continuation review findings

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

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

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

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

* Handle functional continuation cancellation

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

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

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

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

* Simplify functional workflow instance isolation

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

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

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

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

* Scope functional workflow checkpoint storage

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

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

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

* Require building functional workflow instances

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

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

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

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

---------

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

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

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

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

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

* Make approval batches occurrence-safe

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

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

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

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

* Make approval resume retries idempotent

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

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

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

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

* Separate approval execution ownership

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

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

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

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

* Represent approval execution uncertainty

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

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

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

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

* Reconcile approval snapshots with lifecycle state

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

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

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

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

* Bound process-local approval lifecycle state

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

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

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

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

* Complete approval lifecycle cutover

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

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

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

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

* Align AG-UI approval resumes with protocol

* Align workflow approvals with AG-UI resumes

* Address AG-UI approval review findings

* fix AG-UI test typing checks

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

* Update agentserver responses and invocations to x.1.0b1

* Pass platform context to state store provider

* Pass user id

* Correct requirements.txt

* Fix unit tests

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

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

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

Fixes #7633

* Ponytail comment erased

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

* Clarify TODO comment regarding memory method rename

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

---------

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

* fix: address Copilot review comments on release_session

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

* fix (harness): address release_session and review feedback
2026-08-13 19:35:36 +00:00
512 changed files with 35282 additions and 12428 deletions
+127 -2
View File
@@ -1,5 +1,130 @@
# Code ownership assignments
# https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
#
# Policy: a PR needs one approval, and it must come from a code owner of the changed
# files ("Require review from Code Owners" + "Required approvals: 1" on `main`).
# A PR touching several CODEOWNERS patterns will request review from the applicable
# code owners, but an approval from any applicable code owner is sufficient to satisfy
# GitHub's required-code-owner review.
#
# Order matters: the LAST matching pattern wins, so a module rule fully replaces the
# catch-all rather than adding to it. @chetantoshniwal is included on every line as a
# repository-wide fallback owner. All owners on a line have equal approval authority.
#
# CONVENTION: owners are written in the order
# @chetantoshniwal <owner A> <owner B> [...]
# @chetantoshniwal is at the beginning for aesthetics. The owners share equal approval
# power and responsibility.
#
# RULE: every path must list at least two owners besides @chetantoshniwal. An author
# cannot approve their own PR, so a path with a single module owner leaves only Chetan
# to review whenever that owner is the author, which defeats the point of naming a
# module owner.
#
# Samples: owned by all core developers of that language, not by the module a sample
# demonstrates. Any core Python developer can approve any Python sample, and any core
# .NET developer can approve any .NET sample. No dedicated rule is needed -- samples
# fall through to the /python and /dotnet rules, which already list those developers.
#
# Tests: same as samples. Tests that live inside a package (python/packages/<pkg>/tests)
# are covered by that package's rule instead, since they sit under its path.
# Default owners for everything not matched by a module rule below.
* @chetantoshniwal @westey-m
# Repository-level paths: every core Agent Framework developer is a code owner, so any
# one of them can approve. Be explicit now and we can use the AgentFramework team in the future.
/docs/ @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
/*.md @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
/LICENSE @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
/.gitattributes @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
/.gitignore @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
/.github @chetantoshniwal @westey-m @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU @peibekwe @rogerbarreto @SergeyMenshykh
# Repository-level paths that require specific owners
/.devcontainer @chetantoshniwal @westey-m @rogerbarreto @SergeyMenshykh
/declarative-agents @chetantoshniwal @moonbox3 @peibekwe
# Core Python developers: @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU
/python @chetantoshniwal @eavanvalkenburg @giles17 @moonbox3 @TaoChenOSU
# Python packages
/python/packages/a2a/ @chetantoshniwal @giles17 @eavanvalkenburg @moonbox3
/python/packages/ag-ui/ @chetantoshniwal @moonbox3 @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 @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 @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 @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 @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 @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 @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
# .NET projects
/dotnet/src/Aspire.Hosting.AgentFramework.DevUI/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/LegacySupport/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Shared/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.A2A/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Abstractions/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.AGUI/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Anthropic/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.AzureAI.Persistent/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.CopilotStudio/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.CosmosNoSql/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.Declarative/ @chetantoshniwal @peibekwe @westey-m
/dotnet/src/Microsoft.Agents.AI.DevUI/ @chetantoshniwal @peibekwe @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Foundry/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/ @chetantoshniwal @rogerbarreto @westey-m
/dotnet/src/Microsoft.Agents.AI.Harness/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.Hosting/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hosting.AspNetCore/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ @chetantoshniwal @rogerbarreto @SergeyMenshykh @westey-m
/dotnet/src/Microsoft.Agents.AI.Hyperlight/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.LocalCodeAct/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.Mcp/ @chetantoshniwal @westey-m @peibekwe
/dotnet/src/Microsoft.Agents.AI.Mem0/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.OpenAI/ @chetantoshniwal @westey-m @rogerbarreto
/dotnet/src/Microsoft.Agents.AI.Purview/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.Tools.Shell/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.Valkey/ @chetantoshniwal @westey-m @SergeyMenshykh
/dotnet/src/Microsoft.Agents.AI.Workflows/ @chetantoshniwal @peibekwe @rogerbarreto
/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ @chetantoshniwal @peibekwe @rogerbarreto
/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/ @chetantoshniwal @peibekwe @rogerbarreto
/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/ @chetantoshniwal @peibekwe @rogerbarreto
/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/ @chetantoshniwal @peibekwe @rogerbarreto
python/samples/getting_started/azure_functions/ @microsoft/agentframework-durabletask-developers
python/samples/getting_started/durabletask/ @microsoft/agentframework-durabletask-developers
+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
+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
+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@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
category: "/language:${{matrix.language}}"
+3 -3
View File
@@ -111,7 +111,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 +137,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 +184,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"
+7 -7
View File
@@ -269,7 +269,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 +325,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 +337,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 +383,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 +463,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 +528,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!')
+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:
@@ -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.
+69 -8
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.
@@ -342,6 +359,21 @@ that manually replay messages own the equivalent rule: do not resend an approval
### Approval request and resume
- A tool that requires approval does not execute before an approved response.
- With an `AgentSession`, every surfaced local or hosted approval request is stored as an immutable snapshot in one
active model batch. A new surfaced batch replaces an abandoned batch instead of accumulating session state.
- Approval request IDs use the provider function `call_id`, whose conversation-level uniqueness is required for
function-call/result correlation. Duplicate request IDs within one batch are rejected as malformed.
- An inbound response is honored only when its request id matches the pending server-held snapshot.
- Approval requests replayed in inbound message history do not create, replace, or resurrect approval authority.
- The executable call id, tool name, arguments, and local or hosted tool metadata are sourced from the recorded
request, never from the response payload.
- A matched approval response consumes its pending entry once. Unmatched, duplicate, and replayed responses do not
reach local execution.
- Tool lookup uses the recorded name against the current registry. A same-name implementation upgrade is allowed;
removing the name prevents local execution.
- Only the strict boolean `True` grants approval. Missing decisions and non-boolean values are rejection, not consent.
- Direct chat-client invocation without an `AgentSession` preserves pass-through compatibility, matching .NET;
authorization sinks still require strict `True`.
- An approved tool executes exactly once.
- A rejected tool executes zero times and produces one synthetic rejection `function_result` using the original
function `call_id`.
@@ -365,6 +397,14 @@ that manually replay messages own the equivalent rule: do not resend an approval
response for local execution, and leaves hosted approval responses as provider protocol data.
- Hosted AG-UI approval interrupts expose an accept/reject decision only; argument edits are rejected because the
hosted provider executes the server-owned request rather than client-edited arguments.
- AG-UI tool approval resumes accept the standard `approved` decision and full-replacement `editedArgs` payload.
Existing MAF clients remain compatible through the `accepted` decision alias and direct partial argument edits.
- An AG-UI `cancelled` resume is a valid terminal decision, not a run error. In a resume covering parallel open
interrupts, resolved siblings still execute and cancelled calls do not. An identical cancellation retry during
the retained terminal window also completes normally without restoring authority.
- AG-UI Approval State capacity is enforced independently for each trusted application scope. Abandoned pending
authority expires after its configured window, and indeterminate execution records remain non-retryable until
their separate safety window permits reclamation. Reclamation never recreates approval authority.
- A server-issued approval request must not be replayed inline during service-side continuation.
- History providers may retain approval control contents in their backing store for audit, but base history replay
filters them before later model calls.
@@ -413,12 +453,17 @@ that manually replay messages own the equivalent rule: do not resend an approval
| Rejected streaming resume | Rejection result update precedes final text and tool executes zero times. | `test_approval_resume_returns_result_without_mutating_inputs[streaming-rejected]`, `test_streaming_approval_resume_yields_terminal_result_before_model_text[rejected]` |
| Mixed approved/rejected batch | Every call gets one correctly correlated terminal result. | `packages/core/tests/core/test_function_invocation_logic.py::test_rejected_approval` |
| Persisted approval replay | Resume executes with the prior call available. | `test_persisted_approval_messages_replay_correctly` |
| Hosted approval pass-through | Hosted requests/responses are not processed as local calls. | `test_hosted_tool_approval_response`, `test_hosted_mcp_approval_response_passthrough`, `test_mixed_local_and_hosted_approval_flow` |
| Hosted approval pass-through | Hosted requests/responses are bound to the recorded provider request and are not processed as local calls. | `test_hosted_tool_approval_response`, `test_hosted_mcp_approval_response_passthrough`, `test_session_approval_binding_reconstructs_hosted_response`, `test_mixed_local_and_hosted_approval_flow` |
| Approval-time user input | Every user-input request from one approved execution returns in order with assistant role and no extra model call; the execution consumes one call-budget unit. | `packages/core/tests/core/test_harness_tool_approval.py::test_approval_resume_returns_all_user_input_requests_without_another_model_call`, `packages/core/tests/core/test_function_invocation_logic.py::test_approval_resume_user_input_counts_toward_function_call_budget` |
| Mixed terminal result and follow-up input | Completed siblings remain tool-role while only follow-up input requests use assistant-role messages/updates. | `packages/core/tests/core/test_function_invocation_logic.py::test_approval_resume_separates_terminal_results_from_follow_up_requests`, `packages/openai/tests/openai/test_openai_chat_completion_client.py::test_mixed_approval_resume_roles_serialize_function_result_as_tool` |
| Approval-time middleware termination | Terminal result returns with no extra model call in either response mode. | `packages/core/tests/core/test_function_invocation_logic.py::test_approval_resume_honors_middleware_termination` |
| Approval re-entry after iteration budget | Pending approved calls resolve once even when prior model calls consumed `max_iterations`. | `packages/core/tests/core/test_harness_tool_approval.py::test_auto_approval_resolves_after_iteration_budget_is_exhausted` |
| Approval resume with reasoning | Model-bound resume history retains reasoning before the call and terminal result in both modes. | `packages/core/tests/core/test_harness_tool_approval.py::test_approval_resume_replays_reasoning_with_function_call_group` |
| Session-bound substituted response | A response is rebound to the immutable recorded call and cannot replace its call id, tool name, or arguments. | `packages/core/tests/core/test_function_invocation_logic.py::test_session_approval_binding_rebinds_consumes_and_rejects_duplicates` |
| Truthy non-boolean decision | Strings, integers, null, and other non-booleans do not authorize execution. | `packages/core/tests/core/test_function_invocation_logic.py::test_session_approval_binding_treats_truthy_non_boolean_as_rejection`, `packages/core/tests/core/test_types.py::test_function_approval_response_deserialization_rejects_non_boolean_decisions`, `packages/ag-ui/tests/ag_ui/test_message_adapters.py::test_function_approval_requires_real_boolean`, `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_resolve_approval_responses_treats_non_boolean_decision_as_rejection` |
| Active batch replacement | A newly surfaced model batch replaces abandoned approval authority instead of growing session state. | `packages/core/tests/core/test_function_invocation_logic.py::test_session_approval_binding_replaces_abandoned_batch` |
| Duplicate request id | Ambiguous request IDs within one active batch fail explicitly. | `packages/core/tests/core/test_function_invocation_logic.py::test_session_approval_batch_rejects_duplicate_request_ids` |
| Tool registry changes | Same-name upgrades may execute the recorded operation; removing the recorded name executes nothing. | `packages/core/tests/core/test_harness_tool_approval.py::test_approval_resume_allows_same_name_tool_upgrade`, `test_approval_resume_does_not_execute_when_recorded_tool_disappears` |
### Approval correlation and replay
@@ -437,6 +482,8 @@ that manually replay messages own the equivalent rule: do not resend an approval
| Missing result call id | A malformed result does not steal another approval's result. | `test_replace_approval_contents_with_results_skips_results_without_call_id` |
| Empty approval message cleanup | Fully consumed approval messages are removed from normalized model input. | `test_replace_approval_contents_with_results_prunes_emptied_messages` |
| Later stateless turn | A prior terminal approval response cannot execute again. | `test_resolved_approval_response_is_inert_on_later_stateless_turn` |
| Unbound or duplicate response | A response with no pending session request is removed; one request authorizes at most one response. | `test_session_approval_binding_rebinds_consumes_and_rejects_duplicates` |
| Forged inbound request history | A caller-supplied request wrapper cannot replace the server snapshot or resurrect consumed authority. | `test_session_approval_binding_does_not_trust_inbound_request_history` |
| Pending history turn | An unresolved approval batch is omitted atomically from unrelated model input while a later decision can still resume it once. | `packages/core/tests/core/test_harness_tool_approval.py::test_pending_approval_from_file_history_stays_resumable_without_model_orphan` |
| Duplicate function-call prevention | Approval normalization does not create a second call for one round. | `test_no_duplicate_function_calls_after_approval_processing` |
| Rejection call id | Rejection result uses the function call id, not only the approval id. | `test_rejection_result_uses_function_call_id` |
@@ -454,10 +501,17 @@ that manually replay messages own the equivalent rule: do not resend an approval
| Auto-approval callback | Callback receives the original function call and executes the approved set once. | `test_tool_approval_middleware_auto_approval_rule_receives_function_call` |
| Shared call budget | Auto-approved re-entry does not reset `max_function_calls`, and every executed approval group counts even when it pauses for input. | `test_tool_approval_middleware_auto_approved_loops_share_function_call_budget`, `test_approval_resume_user_input_counts_toward_function_call_budget` |
| Standing tool rule | Tool-level approval applies only to later matching tools. | `test_tool_approval_middleware_always_approve_tool_rule` |
| Forged standing rule | An unbound or substituted hosted response cannot create a standing middleware approval rule for caller-selected metadata. | `test_tool_approval_middleware_drops_forged_standing_approval`, `test_tool_approval_middleware_rebinds_hosted_standing_approval` |
| Hosted server boundary | Standing approval does not cross `server_label`. | `test_tool_approval_middleware_standing_rules_include_hosted_server_boundary` |
| Argument-scoped rule | Exact arguments are required; empty arguments are not tool-wide. | `test_tool_approval_middleware_always_approve_tool_with_arguments_rule`, `test_tool_approval_middleware_empty_arguments_rule_is_not_tool_wide` |
| Provider-injected approval tool | A tool added during `before_run` defers to in-run resolution, executes once, and emits one result. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_deferred_provider_tool_executes` |
| AG-UI provider boundary | Completed local approval controls from AG-UI request and snapshot replay are absent from raw chat-client input while deferred and hosted approvals keep their respective in-run/provider paths. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_does_not_forward_resolved_local_approval_control_to_chat_client`, `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_deferred_provider_tool_executes`, `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_canonical_resume_preserves_hosted_approval_for_provider`, `packages/ag-ui/tests/ag_ui/test_run.py::test_filter_local_approval_responses_for_provider_removes_duplicate_completed_controls`, `packages/ag-ui/tests/ag_ui/test_run.py::test_filter_local_approval_responses_for_provider_pairs_reused_call_ids_by_occurrence`, `packages/ag-ui/tests/ag_ui/test_run.py::test_canonical_hosted_approval_resume_rejects_edited_arguments_without_mutating_pending` |
| AG-UI standard approval payload | Agent and workflow tool approvals emit canonical `tool_call` interrupts. `approved` plus full-replacement `editedArgs` executes once and replays idempotently, while legacy `accepted` plus direct partial edits remains supported. Hosted approvals remain decision-only. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_entry_applies_standard_full_replacement_edited_args`, `test_endpoint_agent_approval_replayed_standard_edited_resume_is_idempotent`, `test_endpoint_agent_approval_resume_entry_applies_edited_arguments`, `test_workflow_endpoint_emits_canonical_tool_approval_interrupt`, `test_workflow_endpoint_accepts_canonical_tool_approval_resume`, `test_workflow_endpoint_applies_canonical_approval_edited_args`, `test_workflow_endpoint_accepts_legacy_partial_approval_edits`, `test_workflow_endpoint_hosted_approval_rejects_argument_edits` |
| AG-UI cancellation | A cancelled interrupt executes zero times and completes normally, including an identical retry during retained cancellation state; resolved siblings in the same complete resume still execute once. Workflow cancellation clears both runner correlation and the owning agent executor's pending request so later approvals remain resumable. | `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_cancelled_resume_entry_completes_without_execution`, `test_endpoint_agent_approval_replayed_cancellation_completes_idempotently`, `test_endpoint_agent_approval_mixed_cancelled_and_resolved_resume_executes_resolved_tool`, `test_endpoint_workflow_request_info_cancelled_resume_completes_normally`, `test_workflow_endpoint_cancelled_agent_approval_does_not_block_next_approval` |
| AG-UI 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` |
### Errors, control flow, and limits
@@ -470,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` |
@@ -493,11 +551,12 @@ that manually replay messages own the equivalent rule: do not resend an approval
| Foundry encrypted reasoning opt-in | Foundry clients omit `reasoning.encrypted_content` by default and preserve an explicit caller opt-in. | `packages/foundry/tests/foundry/test_foundry_chat_client.py::test_get_response_does_not_request_encrypted_reasoning_by_default`, `test_get_response_preserves_explicit_encrypted_reasoning_opt_in`, `packages/foundry/tests/foundry/test_foundry_agent.py::test_foundry_agent_basic_call_does_not_request_unsupported_encrypted_reasoning`, `test_foundry_agent_preserves_caller_requested_encrypted_reasoning`, `packages/foundry_hosting/tests/test_responses_int.py::TestReasoningHostedMcpReplay::test_second_turn_replays_mcp_call_with_encrypted_reasoning` |
| Opaque reasoning signature replay | Provider-specific opaque reasoning metadata is captured and restored on reconstructed calls. | `packages/gemini/tests/test_gemini_client.py::test_function_call_part_captures_thought_signature_as_reasoning_content`, `test_reconstructed_function_call_replays_thought_signature_from_reasoning_content` |
| Chat Completions approval wrappers | Framework approval wrappers are not sent as chat messages. | `packages/openai/tests/openai/test_openai_chat_completion_client.py` approval serialization tests |
| AG-UI approval result event | Approved result emits once with content and persists in snapshot. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_approval_resume_emits_tool_call_result`, `test_approval_resume_result_has_content`, `test_approval_resume_snapshot_replaces_approval_payload_with_tool_result`, `test_approval_resume_zero_updates_emits_tool_result` |
| AG-UI rejection/mixed decision | Transport emits only the events defined for approved and rejected calls without duplicates. | `test_rejection_does_not_emit_tool_call_result`, `test_mixed_approve_reject_emits_only_approved_tool_result`, `test_resolve_approval_responses_returns_only_approved` |
| AG-UI approval-time follow-up | The full grouped user-input pause remains in message history and emits no synthetic `TOOL_CALL_RESULT`. | `test_resolve_approval_responses_preserves_follow_up_user_input_group` |
| AG-UI approval execution failure | A grouped executor failure becomes one deterministic terminal error result for the approved call. | `test_resolve_approval_responses_returns_failure_when_grouped_execution_raises` |
| AG-UI no-approval path | Ordinary tool results do not gain an extra approval result event. | `test_no_approval_no_extra_tool_result` |
| AG-UI approval result event | Approved result emits once with content and persists in snapshot. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_approved_call_emits_one_live_result_under_original_identity`, `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_persists_replayable_tool_results`, `test_endpoint_agent_approval_replayed_resume_entry_reprojects_retained_result` |
| AG-UI rejection/mixed decision | Transport emits only the events defined for approved and rejected calls without duplicates. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_rejected_call_does_not_execute_or_emit_live_result`, `test_mixed_batch_preserves_approved_result_identity_and_order`, `packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_rejection_releases_already_approved_sibling` |
| AG-UI approval-time follow-up | The full grouped user-input pause remains in message history and emits no synthetic `TOOL_CALL_RESULT`. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_approval_follow_up_group_remains_in_history_without_live_tool_result` |
| AG-UI approval execution failure | A grouped executor failure becomes one deterministic terminal error result for the approved call. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_approval_execution_failure_emits_one_terminal_error_result` |
| AG-UI no-approval path | Ordinary tool results do not gain an extra approval result event. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_no_approval_path_emits_no_approval_specific_duplicate_result` |
| AG-UI 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` |
@@ -531,6 +590,7 @@ uv run poe syntax -P openai
uv run poe pyright -P openai
uv run poe test-typing -P openai
uv run poe test -P ag-ui
uv run poe test -P declarative
uv run --directory packages/foundry_hosting poe test
```
@@ -556,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
+2 -1
View File
@@ -136,7 +136,8 @@ only to approved first-party endpoints.
| 15 | `core.in_memory_skills_source` | In-memory / programmatic skills | `agent_framework.InMemorySkillsSource` |
| 16 | `core.mcp_skills_source` | MCP-backed skills | `agent_framework.MCPSkillsSource` |
| 17 | `core.session_store` | Agent session store | `agent_framework.SessionStore` / `FileSessionStore` |
| 1831 | _reserved_ | core growth | — |
| 18 | `core.agent_hooks` | Agent Hooks middleware | `agent_framework.create_agent_hooks_middleware` |
| 1931 | _reserved_ | core growth | — |
| 32 | `orchestration.sequential` | Sequential orchestration | `agent_framework_orchestrations.SequentialBuilder` |
| 33 | `orchestration.concurrent` | Concurrent orchestration | `agent_framework_orchestrations.ConcurrentBuilder` |
| 34 | `orchestration.group_chat` | Group-chat orchestration | `agent_framework_orchestrations.GroupChatBuilder` |
+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
+17 -16
View File
@@ -42,25 +42,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="System.CodeDom" Version="10.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.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" />
<!-- 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 +80,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 +94,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 +109,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" />
+7
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" />
@@ -605,6 +610,7 @@
<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" />
@@ -642,6 +648,7 @@
<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" />
+1
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",
+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;
}
}
}
@@ -37,7 +37,7 @@ TextSearchStore textSearchStore = new(vectorStore, "product-and-policy-info", 30
await textSearchStore.UpsertDocumentsAsync(GetSampleDocuments());
// Create an adapter function that the TextSearchProvider can use to run searches against the TextSearchStore.
Func<string, CancellationToken, Task<IEnumerable<TextSearchProvider.TextSearchResult>>> SearchAdapter = async (text, ct) =>
async Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchAdapterAsync(string text, CancellationToken ct)
{
// Here we are limiting the search results to the single top result to demonstrate that we are accurately matching
// specific search results for each question, but in a real world case, more results should be used.
@@ -49,7 +49,7 @@ Func<string, CancellationToken, Task<IEnumerable<TextSearchProvider.TextSearchRe
Text = r.Text ?? string.Empty,
RawRepresentation = r
});
};
}
// Configure the options for the TextSearchProvider.
TextSearchProviderOptions textSearchOptions = new()
@@ -63,7 +63,7 @@ AIAgent agent = aiProjectClient
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available." },
AIContextProviders = [new TextSearchProvider(SearchAdapter, textSearchOptions)],
AIContextProviders = [new TextSearchProvider(SearchAdapterAsync, textSearchOptions)],
// Since we are using ChatCompletion which stores chat history locally, we can also add a message filter
// that removes messages produced by the TextSearchProvider before they are added to the chat history, so that
// we don't bloat chat history with all the search result messages.
@@ -40,7 +40,7 @@ await UploadDataFromMarkdown(afOverviewUrl, "Microsoft Agent Framework Overview"
await UploadDataFromMarkdown(afMigrationUrl, "Semantic Kernel to Microsoft Agent Framework Migration Guide", documentationCollection, 2000, 200);
// Create an adapter function that the TextSearchProvider can use to run searches against the collection.
Func<string, CancellationToken, Task<IEnumerable<TextSearchProvider.TextSearchResult>>> SearchAdapter = async (text, ct) =>
async Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchAdapterAsync(string text, CancellationToken ct)
{
List<TextSearchProvider.TextSearchResult> results = [];
await foreach (var result in documentationCollection.SearchAsync(text, 5, cancellationToken: ct))
@@ -54,7 +54,7 @@ Func<string, CancellationToken, Task<IEnumerable<TextSearchProvider.TextSearchRe
});
}
return results;
};
}
// Configure the options for the TextSearchProvider.
TextSearchProviderOptions textSearchOptions = new()
@@ -72,7 +72,7 @@ AIAgent agent = aiProjectClient
.AsAIAgent(new ChatClientAgentOptions
{
ChatOptions = new() { ModelId = deploymentName, Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief." },
AIContextProviders = [new TextSearchProvider(SearchAdapter, textSearchOptions)],
AIContextProviders = [new TextSearchProvider(SearchAdapterAsync, textSearchOptions)],
// Configure a filter on the InMemoryChatHistoryProvider so that we don't persist the messages produced by the TextSearchProvider in chat history.
// The default is to persist all messages except those that came from chat history in the first place.
// You may choose to persist the TextSearchProvider messages, if you want the search output to be provided to the model in future interactions as well.
@@ -23,7 +23,7 @@ var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ??
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
// A sample function to load the next three calendar events for the user.
Func<Task<string[]>> loadNextThreeCalendarEvents = async () =>
async Task<string[]> LoadNextThreeCalendarEventsAsync()
{
// In a real implementation, this method would connect to a calendar service
return
@@ -32,7 +32,7 @@ Func<Task<string[]>> loadNextThreeCalendarEvents = async () =>
"Team meeting today at 17:00",
"Birthday party today at 20:00"
];
};
}
// Create an agent with an AI context provider attached that aggregates two other providers.
// You must dissable client side conversation storage for clients that support it:
@@ -64,7 +64,7 @@ AIAgent agent = new AIProjectClient(
// The agent will call each provider in sequence, accumulating context from each.
AIContextProviders = [
new TodoListAIContextProvider(),
new CalendarSearchAIContextProvider(loadNextThreeCalendarEvents)
new CalendarSearchAIContextProvider(LoadNextThreeCalendarEventsAsync)
],
});
@@ -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.
@@ -50,6 +50,7 @@ Before you begin, ensure you have the following prerequisites:
|[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
@@ -0,0 +1,6 @@
TRADE CONFIRMATION
Confirmation: TRADE-1234ABCD
Date: 2025-09-12
Symbol: AMZN
Action: BUY
Quantity: 30
@@ -0,0 +1,6 @@
TRADE CONFIRMATION
Confirmation: TRADE-EE11FF22
Date: 2025-01-30
Symbol: GOOGL
Action: BUY
Quantity: 25
@@ -0,0 +1,6 @@
TRADE CONFIRMATION
Confirmation: TRADE-A1B2C3D4
Date: 2024-02-14
Symbol: MSFT
Action: BUY
Quantity: 40
@@ -0,0 +1,7 @@
symbol,shares,cost_basis,purchase_date
MSFT,40,312.50,2023-02-14
AAPL,75,168.20,2022-11-03
NVDA,120,42.80,2021-06-21
AMZN,30,142.10,2023-09-12
GOOGL,25,128.45,2024-01-30
SPY,60,418.90,2024-05-08
1 symbol shares cost_basis purchase_date
2 MSFT 40 312.50 2023-02-14
3 AAPL 75 168.20 2022-11-03
4 NVDA 120 42.80 2021-06-21
5 AMZN 30 142.10 2023-09-12
6 GOOGL 25 128.45 2024-01-30
7 SPY 60 418.90 2024-05-08
@@ -0,0 +1,21 @@
# Production-ready claw (Post 4) — .NET
Post 4 restructures the Step 03 claw into a shared agent library plus three thin hosts.
## Projects
- [`ClawAgent`](./ClawAgent/README.md) — shared factory that builds the full Step 03 HarnessAgent, adds a stable OpenTelemetry source name, and enables Purview only when `PURVIEW_CLIENT_APP_ID` is set.
- [`ClawAgent.Console`](./ClawAgent.Console/README.md) — local interactive console host with console and OTLP OpenTelemetry exporters.
- [`ClawAgent.Hosted`](./ClawAgent.Hosted/README.md) — ASP.NET Responses host for Foundry Hosted Agent deployment. Observability is wired automatically by the hosting runtime; file access and shell are disabled on the container.
- [`ClawAgent.Evals`](./ClawAgent.Evals/README.md) — local finance checks and optional Foundry quality evals.
## Common configuration
```bash
export FOUNDRY_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/api/projects/your-project"
export AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-5.4" # optional
export TOOLBOX_MCP_SERVER_URL="https://.../mcp?api-version=v1" # optional Foundry skills
export PURVIEW_CLIENT_APP_ID="<app-id>" # optional Purview governance
```
OpenTelemetry uses source and meter name `BuildYourOwnClaw.ProductionReady.Claw`. Hosts choose exporters.

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