Compare commits

...

101 Commits

Author SHA1 Message Date
Evan Mattson 7464a59228 Python: Bump Python package versions for 1.11.0 release (#7035)
* Bump Python package versions for 1.11.0 release

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

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

* Fix Gemini streaming type suppression

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

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

* Raise Monty core dependency floor

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

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

* Add agents.md update.

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

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

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

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

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

* updated lock

* Address mem0 OSS application scope

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

* Use filters for mem0 platform add

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-09 16:55:51 +00:00
westey fbaa346eec .NET: Python/.Net: Agent Harness blog post accompanying samples part 3 (#6741)
* Python/.Net: Agent Harness blog post accompanying samples part 3

* Delete inadvertently added files

* Address PR feedback.

* Rename files that are causing dotnet format failures

* Address PR comment

* Fix blog links
2026-07-09 13:52:53 +00:00
dependabot[bot] f5c078b70f .NET: Bump AGUI.Abstractions from 0.0.1 to 0.0.3 (#7007)
* Bump AGUI.Abstractions from 0.0.1 to 0.0.3

---
updated-dependencies:
- dependency-name: AGUI.Abstractions
  dependency-version: 0.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

* Bump AGUI.Formatting, AGUI.Protobuf, AGUI.Client, AGUI.Server from 0.0.1 to 0.0.3

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2026-07-09 13:43:08 +00:00
Sheldon 9f4526a41e fix: parse structured response value from final message (#6383)
Signed-off-by: liuzemei <35027683+liuzemei@users.noreply.github.com>
2026-07-09 11:58:39 +00:00
Sumesh Ramasamy 13fc425bf5 Python: docs: fix removed ChatAgent references in _clients.py docstrings (#6924)
* docs: fix removed ChatAgent references in _clients.py docstrings

* docs: make _clients.py tool-support examples copy/paste-safe

Import Agent in each tool-support protocol docstring example so
copy/pasting no longer raises NameError, and define the shell
executor (LocalShellTool) in the SupportsShellTool example.

Addresses Copilot review feedback on #6924.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: wrap SupportsShellTool example in async function

`async with LocalShellTool()` is a SyntaxError at module level, so the
copy/pasted snippet must live inside an async function to be valid.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Sumesh Bharathi Ramasamy <sumesh@iconicair.io>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 10:16:23 +00:00
Roger Barreto 5f9ac6b394 Python: bind policy-enforcement approvals to a single tool invocation (#6966)
* Bind policy-enforcement approvals to a single tool invocation

PolicyEnforcementFunctionMiddleware retained approved call_ids in a set
that was never cleared, so a reused call_id could re-authorize a later
or different tool call without a fresh approval. It also accepted an
approved response as long as the invocation metadata carried a pending
call_id, without checking the response id or embedded function_call.

Bind each approval to the exact invocation shown for review: call_id,
function name, arguments, the security label (integrity/confidentiality),
and the session. Validate that the approval response itself names the
pending request (its id and embedded function_call), and consume the
approval on first use. A reused call_id, a different function, changed
arguments, an escalated label, a different session, or a mismatched
approved response now all require a fresh approval. Adds regression tests
covering each of those cases plus legitimate re-approval.

* Require approval response identifiers to be present and match

Make the policy-enforcement approval-response check reject a response
that omits its id or embedded function_call.call_id: both must now be
present and equal to the pending call_id, closing a None-identifier
bypass. Adds a regression test.

* Disclose all policy violations in a single approval request

PolicyEnforcementFunctionMiddleware computed the approval decision once
and reused it across the integrity and confidentiality checks, so a call
that violated both policies produced an approval request describing only
the untrusted-context violation and then silently waved the undisclosed
confidentiality violation on replay.

Detect every applicable violation up front and surface them together in a
single approval request, so a granted approval waves only what it
disclosed. The binding (call_id, function, arguments, security label,
session) and consume-once behavior are unchanged. Adds a regression test
covering a combined untrusted-context and confidentiality violation.

* Bind policy approval to the disclosed violation set and fingerprint

A pending policy approval was bound to the call body, security label, and
session but not to the violations it disclosed. Because the violation set
depends on the tool's policy metadata (max_allowed_confidentiality,
accepts_untrusted), a replay could compute a different or larger set after
that metadata changed and execute it under the old approval even though the
user never reviewed that risk.

Record the canonical disclosed violation fingerprint (type plus reason) in
the pending record and require the replay to trip the same set, otherwise
re-request approval disclosing the new set. Also require the approval
response's approved flag to be a strict boolean True so a truthy non-boolean
value is not treated as approval. Adds regression tests for a new violation
appearing on replay, a same-type violation whose disclosed risk worsened,
and a non-boolean approved flag.
2026-07-09 10:01:53 +00:00
Eduard van Valkenburg 1aca7601b8 Python: Add hosting protocol helper surface (#6891)
* Add Python hosting protocol helper surface

Introduce AgentFrameworkState and SessionStore for app-owned hosting routes, add Responses run conversion/rendering helpers, and update the local Responses sample to use native FastAPI routing with streaming support.

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

* Fix CI failures, session continuity, and streaming model reporting

- Fix constrained TargetT TypeVar in AgentFrameworkState: split __init__
  into per-shape overloads (instance/sync factory/async factory/awaitable)
  since a bound TypeVar combined with one big Callable/Awaitable union
  parameter was unsolvable across pyright/pyrefly/ty/zuban.
- Fix _FakeAgent test fixtures to structurally satisfy SupportsAgentRun
  (matching attribute types and overloaded run()), which the above surfaced.
- Add SessionStore.put() to alias an additional session id to an
  already-resolved session, and use it in the local_responses sample to fix
  a real session-continuity bug: previous_response_id rotates every turn,
  so without aliasing the newly minted response id, turn 3+ of a
  conversation silently lost all prior history. Verified against a live
  Foundry model across a 3-turn conversation.
- Fix responses_stream_events_from_run to report the real model instead of
  the "agent" fallback: AgentResponse.from_updates never carries a raw
  representation forward, so capture model from the individual streamed
  updates' raw representations instead. Verified live.
- Add response_model=None to the sample's FastAPI route (it could not boot
  at all: FastAPI tried to build a Pydantic response model from the
  JSONResponse | StreamingResponse return annotation).
- Map responses_to_run's ValueError to HTTP 400 instead of a 500.
- Add HTTP round-trip integration tests (packages/hosting-responses) that
  exercise the same FastAPI + AgentFrameworkState + Responses helper wiring
  as the sample via httpx.ASGITransport, including a regression test for
  the session-continuity fix.
- Add Workflow-target test coverage, SessionStore.put/reset_session tests,
  and TypeError-path coverage to packages/hosting/tests/hosting/test_state.py.
- Extend call_server.py / call_server_af.py to a third conversation turn so
  they actually exercise the continuity chain (previous scripts stopped at
  turn 2, which would never have revealed the bug above).

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

* Simplify session-continuity aliasing: fold put() into get()

Per feedback: the growth of SessionStore was not the problem -- it's
intentional, since OpenAI's previous_response_id is designed to let a
caller continue (fork) from any earlier response, not just the latest
one, so every response id has to stay independently resolvable. That
part stays as-is.

What was too complex was the call site: routes had to manually fetch a
session and then conditionally alias it with a separate put() call.
Folded that into a single get(session_id, alias=...) call instead:

- SessionStore.get() gains an optional `alias` keyword that registers an
  additional id for the same session in the same call (no-op if alias is
  None or equal to session_id). Removed the separate put() method.
- AgentFrameworkState.get_session() passes `alias` through.
- local_responses sample and the HTTP round-trip integration tests now
  do `await state.get_session(lookup_id, alias=response_id)` instead of
  pulling the store out and orchestrating get()/put() by hand.
- Documented that this in-memory SessionStore intentionally never evicts
  (by design, to support forking), and that a storage-backed replacement
  (Redis, a database, ...) is responsible for its own TTL/eviction
  policy.

Verified against a live Foundry model across a 3-turn previous_response_id
chain after the simplification.

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

* Refine hosting state helpers

Split the shared state surface into AgentState and WorkflowState, keep SessionStore and CheckpointStore as plain storage, and make state helpers responsible for get-or-create behavior. Update the Responses sample and HTTP round-trip tests to store the post-run session explicitly under the minted response id, and support WorkflowBuilder/orchestration-style builders via structural build() support.

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

* Fix hosting state test protocol fakes

Widen fake agents' get_session service_session_id parameter to match the SupportsAgentRun protocol under the Python 3.11 test typing checkers.

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

* Simplify Responses stream helper naming

Rename responses_stream_events_from_run to responses_stream_from_run across exports, tests, docs, and the local Responses sample to align with the generic <protocol>_stream_from_run helper convention.

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

* Add state-level storage setters

Add AgentState.set_session and WorkflowState.set_checkpoint_storage so app code can pair get-or-create helpers with explicit post-run storage without reaching into the underlying stores. Update Responses docs, tests, and sample to use state.set_session.

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

* Simplify WorkflowState checkpoint handling

Remove CheckpointStore from WorkflowState so workflow checkpointing uses the existing CheckpointStorage abstraction directly. Keep WorkflowState focused on resolving workflow targets, including builders, and update hosting docs/tests accordingly.

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

* Rename Responses streaming run helper

Rename responses_stream_from_run to responses_from_streaming_run across the hosting-responses exports, tests, docs, and local Responses sample.

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

* Align Python hosting spec with protocol helpers

Rewrite SPEC-002 to match the accepted helper-first hosting ADR and the implementation PR: AgentState, WorkflowState, SessionStore, Responses helpers, app-owned security/state responsibilities, and the minimal FastAPI Responses shape.

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

* Remove old Python hosting channel implementation

Remove the unreleased AgentFrameworkHost/channel implementation, the old hosting-telegram package, and old host/channel samples. Keep agent-framework-hosting focused on AgentState, WorkflowState, and SessionStore, and keep hosting-responses focused on helper-first Responses conversion. Update SPEC-002 to match the accepted helper-first ADR and the implementation surface.

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

* Restore helper-first workflow sample

Rebuild the local Responses workflow sample on the protocol-helper surface, add production-readiness cautions to the local hosting samples, and align file-backed workflow checkpoint/cursor storage under one sample storage root.

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

* Address hosting helper review feedback

Handle streaming failures as terminal Responses SSE events, guard concurrent target/session initialization, and scope workflow sample checkpoint storage per continuation.

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

* Clarify Responses sample continuation behavior

Document unknown conversation_id behavior in the agent sample and make the workflow sample explicitly reject conversation_id while continuing to use responses_session_id for previous_response_id.

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

* Clarify Responses sample option policy

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-09 07:51:47 +00:00
Eduard van Valkenburg 9a5312b278 Python: Add message injection middleware (#6998)
* Python: Add message injection middleware

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

* Python: Ignore informational tool calls for message injection

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

* Python: Preserve per-service history with message injection

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-09 07:47:39 +00:00
Ethan qu 978cfcd9e4 Python: Fix Foundry reasoning MCP compaction (#6907)
* Fix Foundry reasoning MCP compaction

* Address reasoning MCP review feedback

---------

Co-authored-by: godququ5-code <256881196+godququ5-code@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-07-09 07:10:39 +00:00
VectorPeak 7a73455e56 Python: normalize single Anthropic tools (#6903)
* Python: Normalize Anthropic single tools

* Potential fix for pull request finding

Thanks

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

* Python: Address Anthropic review feedback

---------

Co-authored-by: VectorPeak <VectorPeak@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-09 07:05:24 +00:00
Evan Mattson 1b4484829c Python: Add Python package release skill (#6356)
* Add Python package release skill

* Address release skill review feedback

* Address Python release skill review comments

* Resolve Python release skill docs conflict
2026-07-09 06:59:59 +00:00
Eduard van Valkenburg 346d3f0820 Python: Mark hosted tool calls informational-only (#6997)
* Python: Mark hosted tool calls informational-only

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

* Python: Address informational-only review feedback

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

* Python: Preserve approval responses in tool invocation

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-09 06:07:58 +00:00
Evan Mattson 9cc020e486 Python: Add AG-UI FastAPI SSE keepalive support (#6980)
* Python: Add AG-UI SSE keepalive endpoint option

Key decisions: add keepalive_seconds as endpoint-owned FastAPI registration configuration with default 15, accept None as the explicit off switch, validate that non-None values are greater than zero during route registration, and keep agent/workflow runner constructors unchanged. Declare sse-starlette>=3.4.5,<4 as a direct AG-UI dependency without changing the existing StreamingResponse path in this slice.

Files changed: packages/ag-ui/agent_framework_ag_ui/_endpoint.py adds validation and the public endpoint parameter; packages/ag-ui/tests/ag_ui/test_endpoint.py covers default, supported runner shapes, endpoint ownership, and invalid intervals; packages/ag-ui/pyproject.toml and uv.lock add the direct sse-starlette dependency metadata.

Verification: uv run pytest focused keepalive endpoint tests -q; uv run poe test -P ag-ui; uv run poe syntax -P ag-ui -C; uv run poe pyright -P ag-ui; uv run poe validate-dependency-bounds-test -P ag-ui; git diff --check; git diff --cached --check. Also ran validate-dependency-bounds-project --mode both --package ag-ui --dependency sse-starlette; it completed but broadened the lower bound, so the issue-required >=3.4.5,<4 contract was restored and re-locked.

Notes: uv run poe typing -P ag-ui and uv run poe check -P ag-ui currently fail in mypy before checking project files because .venv/lib/python3.13/site-packages/numpy/__init__.pyi uses type-statement syntax while the test mypy profile targets Python 3.11. Local issue file was moved to issues/done/ but not staged.

* Python: Emit AG-UI SSE keepalive comments

Key decisions: switch only enabled AG-UI FastAPI endpoint keepalive responses to EventSourceResponse, keep encoded AG-UI SSE frames as bytes on that path to avoid double encoding, and emit the fixed static SSE comment ': keepalive' while preserving existing SSE headers.

Files changed: packages/ag-ui/agent_framework_ag_ui/_endpoint.py adds the EventSourceResponse enabled path and static comment factory; packages/ag-ui/tests/ag_ui/test_endpoint.py adds an endpoint test for a long output-silent gap, keepalive comments, headers, valid data frames, and no data: data: double encoding.

Verification: uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_keepalive_enabled_emits_static_comment_during_silent_gap -q; focused endpoint pytest selection; uv run poe test -P ag-ui; uv run poe syntax -P ag-ui -C; uv run poe pyright -P ag-ui; git diff --check; git diff --cached --check.

Notes: uv run poe check -P ag-ui still fails in the test-typing mypy phase before project files are checked because .venv/lib/python3.13/site-packages/numpy/__init__.pyi uses type-statement syntax while the mypy test profile targets Python 3.11. Local PRD/Ralph/context artifacts were not staged.

* Python: Preserve disabled AG-UI SSE keepalive behavior

Key decisions: cover keepalive_seconds=None at the FastAPI endpoint seam and assert it preserves the legacy StreamingResponse SSE shape without emitting transport keepalive comments.

Files changed: packages/ag-ui/tests/ag_ui/test_endpoint.py adds disabled keepalive endpoint coverage for headers, valid AG-UI data frames, no keepalive comments, and no data: data: double encoding.

Verification: uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_keepalive_disabled_preserves_streaming_response_shape packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_keepalive_enabled_emits_static_comment_during_silent_gap -q; uv run poe test -P ag-ui; uv run poe syntax -P ag-ui -C; uv run poe pyright -P ag-ui; uv run poe test-typing -P ag-ui --checker pyright; git diff --check.

Notes: no production code changes were needed because the endpoint already branches to the existing StreamingResponse path when keepalive_seconds=None. Local PRD/Ralph/context artifacts were not staged.

* Python: Document AG-UI SSE keepalive behavior

Key decisions: document keepalive_seconds at the FastAPI endpoint seam as a default-enabled transport keepalive with None as the off switch, and record that SSE keepalive emits comments without changing AG-UI events or adding protocol heartbeat events.

Files changed: packages/ag-ui/agent_framework_ag_ui/_endpoint.py expands the public endpoint docstring; packages/ag-ui/AGENTS.md records endpoint-owned keepalive guidance; packages/ag-ui/tests/ag_ui/test_endpoint.py adds a public docstring regression.

Verification: uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_add_endpoint_docstring_describes_keepalive_transport_behavior -q failed before the doc update; focused keepalive endpoint tests passed; uv run poe test -P ag-ui; uv run poe syntax -P ag-ui -C; uv run poe pyright -P ag-ui; uv run poe test-typing -P ag-ui --checker pyright; uv run python scripts/check_md_code_blocks.py packages/ag-ui/AGENTS.md; git diff --check.

Notes: no standalone docs page was added. Local issue bookkeeping was moved to issues/done but not staged; local PRD and Ralph/context artifacts remain unstaged.

* Python: Tighten AG-UI FastAPI dependency bound

* Python: Defer AG-UI keepalive transport imports
2026-07-09 05:54:45 +00:00
Lucas Kim 9ef8fadeac Python: Fix Bedrock non-ASCII escaping in JSON content blocks (#6628)
* Python: Fix Bedrock non-ASCII escaping in JSON content blocks

The Bedrock Converse `json` content block was serialized with
`json.dumps(json_value)`, whose default `ensure_ascii=True` escapes
CJK/emoji/accented characters to `\uXXXX` and surfaces garbled text.
Add `ensure_ascii=False` to match the sibling OpenAI client and the
16+ other call sites across the repo. Includes a regression test.

Closes #6627

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix Bedrock test trailing whitespace

---------

Co-authored-by: kimnamu <kimnamu@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
2026-07-09 05:54:26 +00:00
Vaibhav Patel 23bfa49575 Python: Strip tools from Foundry agent request on the preview path (#6644)
The Foundry service rejects requests that include tool declarations when an
agent is specified (HTTP 400 invalid_payload, "Not allowed when agent is
specified."). RawFoundryAgentChatClient._prepare_options stripped tools,
tool_choice, and parallel_tool_calls only on the non-preview path, so when
allow_preview=True (where the agent identity is bound on the OpenAI client via
get_openai_client(agent_name=...)) the tool fields were still sent and the call
failed.

This client always targets a pre-provisioned agent, so it must never send tool
declarations. Drop the tool fields unconditionally and log a single warning
when the caller supplied tools, noting they are used only for client-side
function dispatch. The non-agent FoundryChatClient (model-based) is unaffected.

Fixes #5130.

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-07-09 01:53:39 +00:00
安妮的心动录 c47f20d9a2 Python: fix: DevUI list[Message] input for declarative ToolAgent entry (#6533) (#6534)
* fix: DevUI list[Message] entry for declarative ToolAgent (#6533)

When a declarative ToolAgent is created with default settings the
entry JoinExecutor declares `input_types = [dict | str | list[Message]
| ActionTrigger | ...]`.  DevUI called `select_primary_input_type`
which returned bare `Message` instead of `list[Message]`, then passed
a single Message to the executor that expects a list — causing a
"cannot handle message of type Message" runtime error.

Changes:
- Add `_is_list_message_type` helper (GenericAlias cannot be used with
  isinstance; get_origin/get_args required).
- Add `_find_chat_message_type` that recursively searches union members
  and returns `list[Message]` in preference to bare `Message`.
- `select_primary_input_type`: first-pass uses `_find_chat_message_type`
  so the declarative entry type is correctly returned as `list[Message]`.
- `generate_input_schema`: returns `{"type":"string"}` for `list[Message]`
  so DevUI renders a plain text box.
- Add `_looks_like_message_dict` heuristic (role present, type=="message",
  or exactly {"input":...}) to distinguish serialised Message payloads
  from structured workflow inputs without false positives.
- `parse_input_for_type`: handle `list[Message]` target — wrap plain
  strings/Message objects, convert lists of dicts item-by-item, pass
  structured workflow inputs through unchanged.
- Add 12 regression tests (57 total pass).

* fix: resolve pyright unknown-type errors in parse_input_for_type

---------

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-07-08 23:08:13 +00:00
gezw ab90300a71 Python: Prefer explicit AG-UI resume payloads (#6360)
* Prefer explicit AG-UI resume payloads

* test: tighten AG-UI resume assertions

---------

Co-authored-by: gezw <26155255+gezw@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
2026-07-08 23:06:13 +00:00
Eduard van Valkenburg c64f8d9e86 Clarify service session ID scoping (#6993)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-08 20:04:08 +00:00
Eduard van Valkenburg 0b49609176 Python: Add progressive MCP disclosure (#6850)
* Add progressive MCP disclosure

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

* Address progressive MCP review feedback

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

* Document progressive MCP loader name collisions

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

* Address progressive MCP review feedback

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

* Fix progressive MCP test typing

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

* Track progressive MCP warning feature id

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

* Document internal typing helper guidance

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

* Fix README stars badge link

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

* Support batch progressive MCP load unload

Allow progressive MCP load_tool and unload_tool to accept either a single tool name or a list of tool names, applying successful changes in batches with per-tool model-visible results.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-07-08 18:39:02 +00:00
malsabbagh05 2aae66d063 Python: use writable runtime directory for Foundry Skills sample (#6606)
* fix: use writable skills download directory

* fix: handle empty skills download directory override

---------

Co-authored-by: malsabbagh05 <malsabbagh05@users.noreply.github.com>
Co-authored-by: Tao Chen <taochen@microsoft.com>
2026-07-08 18:26:43 +00:00
Giles Odigwe 5adc038b16 Python: Add refresh_interval (TTL) to CachingSkillsSource (#6977)
* Python: Add refresh_interval (TTL) to CachingSkillsSource

Port .NET's CachingAgentSkillsSourceOptions.RefreshInterval to the Python
skills cache. Previously CachingSkillsSource cached a source's skill list
indefinitely (only clearing on a failed fetch), so callers had no built-in
way to periodically re-discover skills whose backing source changes at
runtime (notably MCPSkillsSource over the network).

CachingSkillsSource now accepts an optional refresh_interval (timedelta):
a cached list older than the interval is treated as stale and re-fetched on
the next call. When None (default) the cache never expires, so existing
behavior is unchanged. Freshness is measured with a monotonic clock via a
monkeypatchable _monotonic() helper. SkillsProvider.__init__ and from_paths
expose a cache_refresh_interval kwarg threaded into the built-in cache.

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

* Python: Address review feedback on CachingSkillsSource refresh_interval

- from_paths: do not forward cache_refresh_interval when disable_caching=True,
  matching the docstring and avoiding a TypeError for legacy subclass __init__
  signatures.
- Correct docstring/AGENTS.md wording: a failed fetch does not update the cache
  (initial failure leaves it empty; a refresh failure keeps the prior list),
  rather than "resetting"/"leaving empty" in all cases.
- Fix test typing: narrow provider._source via isinstance before accessing
  inner_source/_refresh_interval so ty/zuban/mypy/pyright all resolve them.
- Add regression tests for disable_caching + interval and legacy-subclass paths.

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

* Python: Do not forward cache_refresh_interval from from_paths into __init__

The refresh interval is already baked into the composed CachingSkillsSource
that from_paths builds, and __init__ leaves a caller-supplied source
un-wrapped, so forwarding cache_refresh_interval into cls(...) was a no-op
for caching behavior while breaking legacy subclasses whose __init__ predates
the kwarg (with caching enabled or disabled). Remove the forwarding entirely.

Strengthen the regression test to cover the real break: a legacy subclass
calling from_paths(paths, cache_refresh_interval=...) with caching enabled
must not raise and the composed source still carries the interval.

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

* Python: Drop _monotonic wrapper; call time.monotonic() directly

Address review feedback: remove the _monotonic() helper that existed only to
aid testing. CachingSkillsSource now calls time.monotonic() inline, and the
refresh-interval tests monkeypatch time.monotonic directly.

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

* Python: Restore main's AGENTS.md sections lost in merge resolution

The merge used 'checkout --ours' for AGENTS.md, which took the whole file
from this branch and inadvertently reverted main's non-conflicting additions
(the __init__.pyi tree entry and the 'Root Public API' section). Restore
main's version and re-apply only the intended SkillsSource decorators change
(refresh_interval docs + reworded cache-failure semantics).

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-08 18:17:16 +00:00
Evan Mattson 7f31bcc294 Python: Clear AG-UI queued approvals on cancel (#6947)
* Python: Add AG-UI approval state store

Key decisions: introduce a bounded process-local server-side Approval State store for AG-UI agent approvals; scope pending approval validation by AG-UI thread id plus the endpoint's configured server-side scope when present; fail closed when approval-like resume decisions arrive without matching server-owned pending Approval State, covering replayed and wrong-scope attempts without requiring Thread Snapshot persistence.

Files changed: packages/ag-ui/agent_framework_ag_ui/_approval_state.py adds the approval-only in-memory store and scoped thread-key helper; _agent.py owns the default store; _endpoint.py forwards the configured scope to approval handling independently of snapshot persistence; _agent_run.py keys pending approvals by scoped thread id and rejects approval resumes with missing state; tests/ag_ui/test_endpoint.py covers successful default resumes, replay failure, and wrong-scope failure without a snapshot store.

Verification: uv run pytest focused approval resume tests -q; uv run poe test -P ag-ui; uv run poe syntax -P ag-ui -C; uv run poe pyright -P ag-ui; git diff --check; git diff --cached --check. uv run poe typing -P ag-ui and uv run poe check -P ag-ui currently stop in mypy before package checking on .venv/lib/python3.13/site-packages/numpy/__init__.pyi using Python 3.12 type-statement syntax under the configured Python 3.11 target.

Notes: local issue/PRD planning artifacts were not staged. Follow-up slices still own already-approved sibling release, queued/auto-approved approval state preservation, replayable tool result persistence, synthetic-skip tightening, and final security/invariant coverage.

* Python: Release AG-UI approved siblings on resume

Key decisions: preserve core already-approved approval request groups inside AG-UI server-side Approval State for the visible approval interrupt; restore those siblings as server-generated approval responses only after the visible canonical resume passes server-owned validation; keep cancelled visible approvals fail-closed without executing or fabricating sibling results.

Files changed: packages/ag-ui/agent_framework_ag_ui/_agent_run.py stores hidden already-approved sibling approval requests with pending approval entries and rehydrates them during resume; packages/ag-ui/tests/ag_ui/test_endpoint.py adds mixed approval-batch endpoint coverage for approved, rejected, and cancelled visible approvals.

Verification: uv run pytest focused mixed approval sibling tests -q; uv run pytest neighboring approval endpoint tests -q; uv run poe syntax -P ag-ui -C; uv run poe pyright -P ag-ui; uv run poe test -P ag-ui; git diff --check; git diff --cached --check. uv run poe typing -P ag-ui and uv run poe check -P ag-ui pass pyright/pyrefly/ty/zuban for this change but still stop in mypy before package checking on .venv/lib/python3.13/site-packages/numpy/__init__.pyi using Python 3.12 type-statement syntax under the configured Python 3.11 target.

Notes: local .ralph, PRD, and CONTEXT.md planning artifacts were not staged. Follow-up slices still own queued/auto-approved approval state preservation, replayable tool result persistence, synthetic-skip tightening, and final security/invariant coverage.

* Python: Preserve AG-UI queued approval state

Key decisions: persist only the core tool-approval state bag inside the AG-UI server-side Approval State Store, keyed by the scoped AG-UI approval thread id; restore that approval-only state into each per-run AgentSession before approval resolution; pop server-collected auto-approved responses into validated server-generated approval messages so they execute exactly like resumed approvals without trusting client state.

Files changed: packages/ag-ui/agent_framework_ag_ui/_approval_state.py stores bounded tool approval state alongside pending approval entries; _agent.py passes the shared store into agent runs; _agent_run.py restores/saves tool approval state and drains collected auto-approved responses through existing pending-approval validation; packages/ag-ui/tests/ag_ui/test_endpoint.py covers queued approval surfacing and auto-approved response execution through SSE behavior.

Verification: uv run pytest focused queued/auto approval endpoint tests -q; uv run pytest neighboring approval endpoint tests -q; uv run poe syntax -P ag-ui -C; uv run poe pyright -P ag-ui; uv run poe test -P ag-ui; git diff --check; git diff --cached --check. uv run poe check -P ag-ui still stops in mypy before package checking on .venv/lib/python3.13/site-packages/numpy/__init__.pyi using Python 3.12 type-statement syntax under the configured Python 3.11 target; syntax, pyright, pyrefly, ty, and zuban portions completed successfully before that failure.

Notes: local .ralph, PRD, and CONTEXT.md planning artifacts were not staged. Follow-up slices still own replayable tool result persistence, synthetic-skip tightening, and final security/invariant coverage.

* Python: Persist AG-UI approved tool results

Key decisions: fold approval-resolved function_result messages into AG-UI Thread Snapshot history under their original tool call ids; strip server-generated canonical function_approvals resume controls from replayable snapshots; keep live TOOL_CALL_RESULT emission unchanged while preserving next-turn provider history validity.

Files changed: packages/ag-ui/agent_framework_ag_ui/_agent_run.py adds snapshot merge helpers for approval-resolved tool results; packages/ag-ui/tests/ag_ui/test_endpoint.py covers mixed approval batch resume, hydration, and next-turn replay through observable endpoint behavior.

Verification: uv run pytest focused replayable approval endpoint test -q; uv run pytest neighboring approval replay tests and test_approval_result_event.py -q; uv run poe syntax -P ag-ui -F; uv run poe syntax -P ag-ui -C; uv run poe pyright -P ag-ui; uv run poe test -P ag-ui; git diff --check; git diff --cached --check. uv run poe check -P ag-ui still stops in mypy before package checking on .venv/lib/python3.13/site-packages/numpy/__init__.pyi using Python 3.12 type-statement syntax under the configured Python 3.11 target; syntax, pyright, pyrefly, ty, and zuban portions completed successfully before that failure.

Notes: local .ralph, PRD, and CONTEXT.md planning artifacts were not staged. Follow-up slices still own synthetic-skip tightening and final security/invariant coverage.

* Python: Limit AG-UI synthetic skipped results

Key decisions: treat server-owned Approval State, current approval resume decisions, and existing replayable tool results as non-abandoned tool calls for AG-UI sanitizer repair; keep the defensive skipped-result fallback for genuinely abandoned tool calls; reject client-injected tool results as insufficient to satisfy pending server-owned Approval State.

Files changed: packages/ag-ui/agent_framework_ag_ui/_message_adapters.py adds protected tool-call context to synthetic skip injection; packages/ag-ui/agent_framework_ag_ui/_agent_run.py derives protected ids from pending approvals and stored approval-only state; packages/ag-ui/tests/ag_ui/test_message_adapters.py and test_endpoint.py cover protected pending calls, resume decisions, abandoned-call repair, and forged tool-result behavior.

Verification: uv run pytest focused sanitizer red/green tests -q; uv run pytest focused pending-approval endpoint tests -q; uv run pytest package sanitizer plus neighboring approval endpoint tests -q; uv run poe syntax -P ag-ui -C; uv run poe pyright -P ag-ui; uv run poe test -P ag-ui; git diff --check; git diff --cached --check. uv run poe typing -P ag-ui passes pyright/pyrefly/ty/zuban but still stops in mypy before package checking on .venv/lib/python3.13/site-packages/numpy/__init__.pyi using Python 3.12 type-statement syntax under the configured Python 3.11 target.

Notes: local .ralph, PRD, and CONTEXT.md planning artifacts were not staged. Follow-up slice still owns final AG-UI approval repair security and exact-once invariant coverage.

* Python: Verify AG-UI approval invariants

Key decisions: cover final AG-UI approval repair invariants at the FastAPI endpoint seam; treat wrong-thread resumes, client-supplied approval message spoofing, and client-injected approval state as non-executing fail-closed paths; assert exact-once replayable tool results for completed approval batches; document that Approval State is process-local and production authentication, authorization, and deployment/storage durability remain application responsibilities.

Files changed: packages/ag-ui/tests/ag_ui/test_endpoint.py adds endpoint-observable security and exact-once coverage; packages/ag-ui/README.md documents Approval State production responsibilities.

Verification: uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py -q -k 'approval_resume_wrong_thread or approval_function_name_mismatch_message or approval_argument_mismatch_message or approval_client_fields_do_not_mutate or approval_resume_persists_replayable_tool_results'; uv run poe syntax -P ag-ui -F; uv run poe syntax -P ag-ui -C; uv run poe pyright -P ag-ui; uv run poe test -P ag-ui; uv run poe test-typing -P ag-ui --checker pyright; git diff --check; git diff --cached --check.

Notes: local .ralph, PRD, and CONTEXT.md planning artifacts were not staged. This completes the final AG-UI approval repair security and invariant coverage slice.

* Python: Clear AG-UI queued approvals on cancel

* Python: Address AG-UI approval review feedback
2026-07-08 18:04:30 +00:00
Willow Lopez 62456f044d Python: Fix web_search_options sent to Azure OpenAI Chat Completions API (issue #3629) (#6225)
* Fix: Skip web_search_options for Azure OpenAI Chat Completions API

Azure OpenAI Chat Completions API does not support the web_search_options
parameter. Sending it results in a 400 error: 'Unknown parameter:
web_search_options'.

This fix:
- Stores the use_azure_client flag during initialization
- In _prepare_tools_for_openai, skips web search tools when the client
  is Azure-based, logging a warning that guides users to the Responses
  API (OpenAIChatClient) for web search support on Azure

Closes #3629

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: raise ValueError instead of silently ignoring web search on Azure

Address review feedback: silent logger.warning was too easy to miss.
Raising ValueError ensures callers know immediately that web search is
incompatible with Azure Chat Completions and directs them to the
Responses API alternative.

- Changed logger.warning to ValueError in _prepare_tools_for_openai
- Added test_prepare_tools_with_web_search_on_azure_raises
- Added test_prepare_tools_with_web_search_on_openai_allowed

* Fix Azure web search test regex

---------

Co-authored-by: Autumn <Autumn@Autumns-MacBook-Air.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-07-08 17:44:08 +00:00
Giles Odigwe 1249275997 Python: Remove experimental marker from Skills API (#6974)
* Python: Remove experimental marker from Skills API

Promote the Skills feature from experimental to stable, mirroring
.NET PR #6861. Removes the @experimental(SKILLS) decorators from the
skills APIs and the SKILLS ExperimentalFeature enum member, updates
tests and samples accordingly. MCP skills (MCP_SKILLS) remain
experimental, matching the .NET change.

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

* Add experimental-stage assertions for MCP skills types

Guard MCPSkill, MCPSkillResource, and MCPSkillsSource against accidental
promotion by asserting their docstring warning block and
__feature_stage__/__feature_id__ metadata remain experimental (MCP_SKILLS).

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

* Remove redundant stable-stage test for Skills API

Drop TestSkillsStableStage: asserting the absence of experimental
markers on a released API is not meaningful, and the feature-stage
decorator machinery is already covered by test_feature_stage.py.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-08 17:34:34 +00:00
Adam Lin f280742c01 Python: Samples: deterministic action-boundary validation middleware (#5366) (#6528)
* Python: add ATR validation FunctionMiddleware sample (execution-boundary validation, #5366)

Adds python/samples/02-agents/middleware/atr_validation_middleware.py: a
FunctionMiddleware that validates tool arguments at the execution boundary and
raises MiddlewareTermination before call_next() when they match an attack
pattern, so the tool never runs. This is the deterministic, single-enforcement-
point pattern named in #5366 and answers its open follow-up about a recommended
validation-at-execution-boundary sample.

The check is a small self-contained deny-list mirroring Agent Threat Rules (ATR)
intent (prompt injection, exfiltration, credential access in tool args); a
docstring notes how to swap in the full open ruleset via pyatr. No external
dependency, so the sample stays import-clean.

Updates the middleware README Files table.

Signed-off-by: Adam Lin <adam@agentthreatrule.org>

* Python: Samples: run the real ATR engine in atr_validation_middleware

Address review on #6528:
- Load and run the real ATR ruleset via pyatr (ATREngine + AgentEvent
  tool_call event) instead of re-implementing a regex deny-list; the
  built-in deny-list is now only a fallback when pyatr is not installed.
- Add re.DOTALL (and a whole-text scan) to the fallback patterns so
  multiline injection payloads are not missed.
- Move load_dotenv() into main() so importing the module has no side
  effects.
- Route the middleware block/allow messages through a module logger
  instead of print().
- Include the matched ATR rule id in the log and in the
  MiddlewareTermination message for auditability.
- Update the middleware README entry to match.

* fix(samples): make ATR validation middleware pass ty/pyrefly typing CI

Resolve the three type-checker errors flagged on the samples typing jobs
(ty + pyrefly, reportMissingImports/reportAttributeAccessIssue via pyright):

- pyatr is an optional, unstubbed runtime dependency that is not installed
  in the typing CI env; mark its imports with `# type: ignore` so the
  unresolved-import error is suppressed while keeping the graceful
  ImportError -> deny-list fallback intact.
- Replace the function-attribute engine cache
  (`_detect_with_atr._engine`), which ty/pyrefly reject, with a clean
  `functools.lru_cache`-backed `_load_atr_engine()` loader.
- Type the argument-scanning helpers to accept the real
  `FunctionInvocationContext.arguments` type (`BaseModel | Mapping[str, Any]`)
  and normalise a pydantic model via `model_dump()` before scanning, fixing
  the invalid-argument-type error.

ty / pyrefly / pyright (samples config) / ruff check + format all clean on
the file; runtime block/allow behaviour verified for both dict and BaseModel
arguments.

* Python: Samples: simplify ATR middleware to plain pyatr import

Address review feedback (@eavanvalkenburg): now that the sample runs the
real pyatr engine, drop the optional-import scaffolding.

- Add a dependency header declaring pyatr (pip install pyatr).
- Switch to a plain top-level `import pyatr` and remove the
  try/except ImportError fallback path.
- Remove the regex deny-list (_FALLBACK_PATTERNS, _detect_with_fallback);
  keep 2-3 representative pattern shapes inline as a reference comment so
  readers still see the kind of rules ATR encodes. Detection is now a
  single straight-line engine call.
- Keep the prior typing fixes: `# type: ignore` on the pyatr import
  (unstubbed, absent in the typing CI env), the functools.lru_cache
  engine loader, and the BaseModel | Mapping[str, Any] signatures.

* fix: use PEP 723 inline script metadata for sample dependencies

---------

Signed-off-by: Adam Lin <adam@agentthreatrule.org>
Co-authored-by: eeee2345 <eeee2345@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-07-08 11:19:45 +00:00
Eduard van Valkenburg 0438ee61c6 Python: Refocus hosting channels ADR on protocol helpers (#6837)
* Revise Python hosting channels ADR

Refocus the accepted-but-unreleased Python hosting channels ADR on protocol-specific Agent Framework conversion helpers and an optional execution-state host instead of a channel route-contribution framework.

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

* Align hosting ADR with split state helpers

Update the protocol-helper ADR to reflect AgentState and WorkflowState, plain SessionStore and CheckpointStore behavior, explicit post-run session storage, workflow checkpoint storage, and direct WorkflowBuilder/orchestration-builder support.

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

* Generalize protocol helper taxonomy

Add protocol-neutral helper families for run conversion, result rendering, streaming, session-id extraction, and command/action parsing. Classify protocol-specific helpers based on quick scans across Activity/Bot Framework, Discord, A2A, and MCP.

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

* Simplify stream helper naming

Use the single <protocol>_stream_from_run(...) helper naming convention in the hosting protocol-helper ADR.

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

* Use state-level storage helpers in hosting ADR

Update ADR examples so app code calls AgentState.set_session and WorkflowState.set_checkpoint_storage instead of reaching into underlying stores directly.

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

* Address hosting ADR review comments

Clarify fail-closed Foundry isolation helpers, fix workflow checkpoint resume examples, describe durable checkpoint cursor storage, add caller-owned session authorization comments, and switch the Django sketch to an async view.

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

* Simplify workflow checkpoint state in hosting ADR

Keep WorkflowState focused on resolving workflow targets, use existing CheckpointStorage directly, describe app-owned checkpoint cursor storage, and mark appendix code as minimum-shape sketches rather than runtime-ready samples.

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

* Rename stream helper convention

Use <protocol>_from_streaming_run(...) as the protocol-helper naming convention for rendering streaming run output.

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

* added notes on state and continuity

* updates based on review

* added consulted

* updates based on review

* remove pyright for illustrative code

* Add streaming to Responses ADR sketch

Extend the FastAPI appendix sketch with the streaming branch and note that the Django sketch omits streaming to avoid duplicating the same state/finalization pattern.

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

* added note on extending the server

* added note on responsible for

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-08 09:59:50 +00:00
westey 76f2c1a0c9 .NET: [BREAKING] Graduate per-service-call persistence and approval-not-required function bypassing (#6970)
* Remove experimental flags for RequirePerServiceCallChatHistoryPersistence and DisableApprovalNotRequiredFunctionBypassing

* Address PR comments

* Fix failing test
2026-07-08 09:31:33 +00:00
Eduard van Valkenburg 094d8d209a Python: Lazy load root agent_framework exports (#6962)
* Lazy load root agent_framework exports

Move the root public API to lazy runtime exports backed by a typed stub, keep Runner deprecation handling in the owning workflow runner module, and document the maintenance pattern.

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

* Tighten harness factory typing

Add a private harness stub so create_harness_agent has a fully known public signature without depending on agent-framework-tools at runtime.

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

* Address lazy root export review comments

Harden the circular import guard and add root export smoke tests covering representative lazy imports, star imports, and root stub export synchronization.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-08 09:20:27 +00:00
Eduard van Valkenburg 783e8c4568 Python: skip NumPy stubs in mypy typing (#6969)
Mypy intentionally targets Python 3.10 for test typing, but NumPy 2.5 stubs include Python 3.12 type statement syntax. Skip following NumPy stubs so dependency maintenance can validate the repository tests without parsing NumPy internals.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-08 09:13:59 +00:00
Evan Mattson 12b029858e Build(deps): consolidate Dependabot dependency updates (#6984)
* Consolidate Dependabot dependency updates

* Restore method assignment suppression
2026-07-08 09:09:03 +00:00
Eduard van Valkenburg e26c8591ae Python: Fix response metadata construction (#6955)
* Python: Fix response metadata construction

Propagate complete AgentResponse metadata through core response construction and provider finalization paths.

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

* Python: Refine Ollama response metadata parsing

Filter Ollama usage details to real token counts and only propagate streaming finish metadata from final chunks.

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

* Python: Address response metadata review comments

Accumulate Copilot non-streaming usage events and keep structured response value parsing lazy for provider hooks.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-08 09:05:50 +00:00
Benke Qu 7f551f057a Python: fix invalid options kwarg in workflow shared session sample (#6294)
* fix: use client_kwargs instead of invalid options kwarg in workflow sample

Workflow.run() does not accept an options parameter. The store=False
kwarg was silently ignored. Use client_kwargs to correctly forward it
to the underlying chat client.

Fixes #6293

* fix: use backend-neutral wording in client_kwargs comment

---------

Co-authored-by: Benke Qu <bequ@microsoft.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-07-08 08:13:57 +00:00
Rayan Salhab 0661c69b78 fix(ag-ui): preserve streamed text message id (#6269)
Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-07-08 08:12:27 +00:00
Alireza Afzali 2889475bce docs: add prerequisite commands for Python hosting samples (#5935)
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-07-08 08:08:53 +00:00
Minh Vu 339bbeb881 Fix DevUI deployment Dockerfile auth args (#6150)
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
2026-07-08 08:06:30 +00:00
t-anjan 5e0793542d Python (anthropic): Migrate structured outputs to GA output_config.format (#5884)
Bug
---
`RawAnthropicClient._prepare_options` forwards `response_format` as the
**deprecated** beta parameter `output_format={"type": "json_schema", "schema":
{...}}` plus the beta flag `structured-outputs-2025-11-13`. When the same
request also includes `tools`, Claude emits concatenated / malformed JSON —
e.g. three copies of the schema's empty default like
`{"matches":[]}{"matches":[]}{"matches":[]}` — instead of populating the
schema. Anthropic's GA shape — `output_config={"format": {"type":
"json_schema", "schema": {...}}}` — works correctly with tools.

Verified empirically on `agent-framework-anthropic` against
`claude-sonnet-4-6` for a structured-output workload that combined
`response_format` with a tool (`run_shell`); the deprecated path produced
the malformed concatenated output, the GA path did not.

Changes
-------
- Move `response_format` into `run_options["output_config"]["format"]` and
  stop adding the `structured-outputs-2025-11-13` beta flag (the GA path
  doesn't need it).
- Merge the format into any caller-supplied `output_config` so e.g.
  `output_config["effort"]` (adaptive-thinking effort level) survives the
  transformation.
- Drop the now-unused `STRUCTURED_OUTPUTS_BETA_FLAG` constant (private to
  this module — no external callers).
- `_prepare_response_format` keeps the same `{"type": "json_schema",
  "schema": ...}` return shape; the docstring is updated to point at the
  GA target.

Test plan
---------
- `uv run pytest packages/anthropic/tests` → 130 passed.
- New tests:
  - `test_prepare_options_uses_output_config_for_response_format` — the
    GA `output_config.format` shape is emitted, the deprecated
    `output_format` key is not, and the `structured-outputs-2025-11-13`
    beta flag is not added.
  - `test_prepare_options_preserves_caller_supplied_output_config_effort`
    — a caller-supplied `output_config["effort"]` survives the merge.
  - `test_prepare_options_no_response_format_omits_output_config` — no
    `output_config` is added implicitly when `response_format` is absent.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-08 07:47:48 +00:00
Kranthi Manchikanti de39be9e58 Python: Improve error message when TypeVar is used in handler registration (#4553)
* Python: Improve error message when TypeVar is used in handler registration

Fixes #4547. Adds early detection of unresolved TypeVar instances in:
- @handler decorator (both explicit and introspected type paths)
- @executor decorator (both explicit and introspected type paths)
- WorkflowContext type argument validation (direct and union members)

When a TypeVar is detected, a clear ValueError is raised with actionable
guidance to use concrete types via @handler(input=ConcreteType, output=ConcreteType).

* Address PR review: runtime-safe TypeVar detection and unit tests

- Add shared is_typevar() helper in _typing_utils.py that safely detects
  TypeVar from both typing and typing_extensions modules
- Replace all isinstance(x, TypeVar) calls with is_typevar() in
  _executor.py, _function_executor.py, and _workflow_context.py
- Add 18 unit tests covering TypeVar validation for @handler, @executor,
  and WorkflowContext[T] (explicit params, introspection, union members)

* Fix pyright error: add type annotation to _TYPEVAR_TYPES

Pyright's reportUnknownVariableType flagged the inferred type as
partially unknown. Adding an explicit `tuple[type, ...]` annotation
resolves the strict-mode check.

* Suppress pyright reportUnknownVariableType for _TYPEVAR_TYPES

Pyright cannot infer the runtime type of TypeVar constructors, so the
tuple elements resolve to type[Unknown]. A type annotation alone does
not satisfy strict mode — add an inline suppression for this specific
diagnostic since the unknown types are intentional (runtime TypeVar
class detection).

* Reject nested TypeVars in workflow annotations

---------

Co-authored-by: Kranthi Kumar Manchikanti <kmanchikanti@microsoft.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
2026-07-08 07:32:54 +00:00
Matt Van Horn fb4be3bb1f Python: request reference source data in agentic search (#5095) (#5100)
Pass knowledge_source_params with include_reference_source_data=True for
each resolved knowledge source on the KnowledgeBaseRetrievalRequest, so
ref.source_data is populated when the source has source_data_fields
configured. Uses SearchIndexKnowledgeSourceParams (azure-search-documents
12.0.0) and resolves real source names for both created and existing
knowledge bases (avoids the prior 'None-source' name).

Fixes #5095

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-08 05:05:54 +00:00
Tao Chen b31c8981a4 Python: Add multi-tenant hosting hosting security consideration to a2a sample (#6983)
* Add multi-tenant hosting hosting security consideration to a2a sample

* Potential fix for pull request finding

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

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-08 03:27:09 +00:00
Giles Odigwe 96e009f87a Python: Forward skill_directories and disabled_skills to Copilot session (#6937)
GitHubCopilotAgent never forwarded the Copilot SDK's skill_directories
(and disabled_skills) parameters to create_session/resume_session, so
native Copilot CLI skills could not be configured through the agent.

Add both as fields on GitHubCopilotOptions and forward them (with
runtime-override and empty-list-clears-defaults semantics matching
instruction_directories) in _create_session and _resume_session.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-08 01:59:11 +00:00
Evan Mattson 93189e437d Remove unnecessary design doc in root (#6945) 2026-07-08 10:33:19 +09:00
Javier Calvarro Nelson 3d17615b6e .NET: Replace MAF AG-UI abstractions with the AG-UI C# SDK abstractions (#6653)
* .NET: Replace internal AG-UI implementation with external ag-ui packages

Remove the in-tree Microsoft.Agents.AI.AGUI sources and consume the external
AG-UI .NET SDK packages (AGUI.Abstractions, AGUI.Formatting, AGUI.Protobuf,
AGUI.Client, AGUI.Server) at 0.1.0-preview instead.

- Microsoft.Agents.AI.Hosting.AGUI.AspNetCore keeps its own ASP.NET glue
  (MapAGUI / AddAGUI / SSE result) layered over the framework-agnostic
  AGUI.Server primitives (ToChatRequestContext / AsAGUIEventStreamAsync).
- Migrate call sites to the options-based AGUIChatClient constructor and recover
  the originating AG-UI input via ChatOptions.TryGetRunAgentInput.
- Multi-turn continuation flows through parentRunId + threadId on
  RawRepresentationFactory; shared state flows through RunAgentInput.State and is
  surfaced as StateSnapshotEvent raw representations.
- Update samples, hosting/unit/integration tests, and central package versions.

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

* Add migration README for removed Microsoft.Agents.AI.AGUI package

Keep the package folder in place with a README explaining that the in-tree AG-UI protocol abstractions moved to the external AGUI.* NuGet packages, with a mapping of old namespaces to the new packages and a migration guide.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
2026-07-07 20:09:53 +00:00
westey 03a96faf43 .NET: Add security information to harness features xml docs (#6933)
* Add security information to harness features xml docs

* Address PR comments
2026-07-07 17:50:55 +00:00
Eduard van Valkenburg 07981847ed Add chat client Agent typing tests (#6950)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-07 17:15:22 +00:00
Ahmed Muhsin 81e425b44f Python: [BREAKING] Durable Task multi-workflow hosting and sub-workflows (#6696)
* feat(durabletask): add workflow naming helpers (multi-workflow phase 0)

Foundation for hosting multiple workflows (and later sub-workflows) on one
durable task host. Adds a host-agnostic naming module that derives the stable
durable names a hosted workflow registers under.

- New `_workflows/naming.py`:
  - `workflow_orchestrator_name(name)` -> `dafx-{name}` (orchestration name,
    aligned byte-for-byte with .NET `WorkflowNamingHelper`).
  - `workflow_name_from_orchestrator(name)` -> reverse, `None` when not prefixed.
  - `validate_workflow_name(name)` -> rejects empty / malformed / auto-generated
    `WorkflowBuilder-<uuid>` names (validate-and-reject rather than silently
    sanitize, since the name becomes a durable identity and an HTTP route segment).
  - `is_auto_generated_workflow_name(name)`, `DURABLE_NAME_PREFIX`.
- Export the helpers from the package public API.
- Mark `WORKFLOW_ORCHESTRATOR_NAME` deprecated in favor of per-workflow names
  (kept functional; the single-workflow path still uses it until phase 1).
- 39 unit tests covering round-trips and validation.

Design: docs/design/durabletask-multiworkflow-and-subworkflows.md

* feat(durabletask): host multiple workflows per worker with scoped names (phase 1)

Enables hosting more than one MAF workflow on a single standalone Durable Task
worker, and aligns both hosts on workflow-scoped durable names so two co-hosted
workflows that reuse an executor id cannot collide.

Naming (shared, host-agnostic):
- orchestration: dafx-{workflowName} (matches .NET; the name DT tooling surfaces)
- non-agent activity / agent entity: dafx-{workflowName}-{executorId} (scoped)
- New naming helpers workflow_scoped_executor_id / workflow_executor_activity_name.

Standalone worker (agent-framework-durabletask):
- configure_workflow is now additive: stores workflows keyed by Workflow.name,
  rejects duplicate / auto-generated (WorkflowBuilder-<uuid>) / invalid names,
  registers one orchestrator per workflow plus its scoped activities/entities.
- The shared orchestrator dispatches scoped names derived from workflow.name.
- New registered_workflow_names property.

Client (DurableWorkflowClient):
- Optional default workflow_name on the client; start/run/stream accept a per-call
  workflow_name and target dafx-{name}.
- Opt-in ownership validation on status/HITL methods: when a workflow name is
  resolvable, an instance whose orchestration name does not match is treated as
  not-found (status -> None, pending -> [], send_hitl_response / await -> raise),
  mirroring the Azure Functions route-scoping check.

Azure Functions host (agent-framework-azurefunctions):
- Registration now uses the same scoped names so the shared orchestrator's
  dispatch matches (single workflow per app for now; flat workflow/* routes kept).
- Workflow name is validated up front; workflow agents register under the scoped
  entity id; _is_workflow_orchestration scopes to dafx-{workflow.name}.

Samples + tests:
- Durable Task and Azure Functions workflow samples now name their workflow.
- Unit tests cover multi-workflow registration, name validation, client targeting,
  and ownership; integration tests target the named workflows.

WORKFLOW_ORCHESTRATOR_NAME remains exported (deprecated). This is a hard switch:
in-flight single-workflow instances created before upgrade (under the old
workflow_orchestrator name) will not resume.

Design: docs/design/durabletask-multiworkflow-and-subworkflows.md

* feat(azurefunctions): host multiple workflows per app with per-workflow routes (phase 2)

Completes multi-workflow hosting on the Azure Functions host, building on the
shared scoped-naming foundation from the worker phase.

AgentFunctionApp:
- New `workflows=` parameter accepting a list (keyed by each `Workflow.name`) or a
  name->Workflow mapping; the existing `workflow=` is a single-workflow alias.
  Both may be combined. Duplicate names and mapping-key/name mismatches are rejected.
- Each workflow registers its own `dafx-{name}` orchestration, workflow-scoped
  activities/entities, and per-workflow HTTP routes:
  `workflow/{name}/run`, `workflow/{name}/status/{instanceId}`,
  `workflow/{name}/respond/{instanceId}/{requestId}`. Routes are always
  per-workflow (even for a single workflow) so callers don't change URLs as an app
  grows from one workflow to many.
- Route ownership check is per-workflow (`_is_owned_orchestration(status, name)`):
  a leaked instance id for another orchestration -- or another workflow -- is
  treated as not-found, extending the route-scoping defense.
- `get_agent(context, name, workflow_name=...)` resolves a workflow agent under its
  scoped id; bare `agents=` registration keeps the standalone surface. New
  `workflows` introspection property; `.workflow` now returns the sole workflow
  (or None when several are hosted).
- Removed the now-unused flat-URL helper `_build_status_url` (handlers inline
  per-workflow URLs).

Samples + tests:
- Azure Functions workflow samples (09-12) name their workflow; integration tests
  target the per-workflow routes.
- Unit tests cover multi-workflow registration, duplicate/mapping/auto-name
  rejection, and per-workflow ownership.

Note: sample README / demo.http route docs are updated in the docs phase.

Design: docs/design/durabletask-multiworkflow-and-subworkflows.md

* feat(durabletask): sub-workflows via durable child orchestrations (phase 3)

Run WorkflowExecutor nodes as durable child orchestrations on both hosts.

- Protocol: add call_sub_orchestrator to WorkflowOrchestrationContext, implemented by the durabletask and Azure Functions adapters.

- Registration: planner classifies WorkflowExecutor as subworkflow_executors; collect_hosted_workflows walks nested workflows (parent first, deduped by name). Both hosts recursively register every nested workflow's orchestration/agents/activities once; only top-level workflows get HTTP routes. Names validated up front before any registration side effects.

- Orchestrator: dispatch WorkflowExecutor nodes via call_sub_orchestrator(dafx-{innerName}) with deterministic child instance ids ({instanceId}::{executorId}::{counter}), a trusted-input marker carrying nesting depth (bounded at 25), and outputs routed as messages (default) or parent outputs (allow_direct_output).

- Tests: registration/collect, orchestrator prepare/process/unwrap, recursive registration on both hosts. Sample: 11_subworkflow.

* feat(durabletask): sub-workflow HITL via qualified request ids (phase 4)

Surface a nested sub-workflow's human-in-the-loop request behind the top-level instance (B2 single addressing surface).

- Orchestrator records dispatched sub-workflow child instance ids in its custom status (subworkflows map) before suspending in task_all, so the read side can reach a child's pending request while the parent is paused.

- Read side (durabletask client get_pending_hitl_requests; AF status route) recurses into nested child statuses, qualifying each nested request id as {executorId}::{requestId} (accumulated for deeper nesting).

- Write side (durabletask client send_hitl_response; AF respond route) splits a qualified id on '::', resolves the owning child orchestration via the parent's subworkflows map, and raises the event on the leaf child with the bare request id. Unknown/inactive sub-workflow -> error/404.

- Shared SUBWORKFLOW_REQUEST_SEPARATOR ('::') in naming so both hosts and the client agree. respondUrl/respond always targets the top-level instance.

- Tests: TestSubworkflowHitl (durabletask client, 7), TestAgentFunctionAppSubworkflowHitl (AF, 7). Sample: 12_subworkflow_hitl (HITL pause inside an embedded sub-workflow).

* docs(durabletask): ADR + sample route docs for multi-workflow and sub-workflows (phase 5)

- Add ADR-0030 capturing the multi-workflow and sub-workflow hosting decisions (naming, scoped inner names, per-workflow routes, child-orchestration sub-workflows, hard-switch migration, B2 sub-workflow HITL, scoped agent addressing) with considered alternatives; mark the design doc as implemented and link the ADR.

- Update Azure Functions workflow samples (09-12) README/demo.http to the per-workflow route shape (workflow/{name}/run|status|respond) introduced in phase 2.

- Extend the durabletask sample catalog with the workflow hosting patterns (08-12), including the new 11_subworkflow and 12_subworkflow_hitl samples.

* fix(durabletask): harden sub-workflow hosting + add sub-workflow integration tests

Post-review hardening of the multi-workflow / sub-workflow durable hosting:

- Trust boundary: strip the reserved sub-workflow envelope key from untrusted
  client input at both host boundaries (DurableWorkflowClient.start_workflow and
  the AF start route) so a forged envelope cannot reach the trusted pickle path.
- Nested HITL addressing: qualify nested pending requests by (executorId, ordinal)
  using a '~' separator (was '::', which collided with core's auto::N functional
  request ids); the parent status subworkflows map is now a per-executor list so
  multiple children dispatched in one superstep stay independently addressable.
- Reject two different workflow instances that share a name (the same instance
  reused by sibling nodes is still deduped); validate executor ids (separator-free,
  length-bounded) when hosting durably.
- Remove the arbitrary sub-workflow nesting depth cap: a WorkflowExecutor wraps a
  concrete Workflow so the nesting tree is finite at build time, and the durable
  instance-id length limit is the natural ceiling (matches .NET, which has none).

Tests/samples:
- New durabletask integration tests for sub-workflow composition (11) and nested
  sub-workflow HITL (12); new no-agent AF sub-workflow HITL sample (13) + test.
- Exempt no-agent samples from the model-credential gate in both integration
  conftests so the nested-HITL plumbing is covered deterministically.
- Update durabletask sample 12 docs to the new qualified-id format.

Validated: 484 unit tests; durabletask integration 08/09/11/12 and AF 12/13 pass
against the live emulators; pyright 0 errors; ruff clean.

* fix(durabletask): address PR review feedback on naming, typing, and docs

- Unquote df.DurableOrchestrationClient annotations so pyupgrade passes.
- Narrow the split_subworkflow_request_id result before unpacking in a naming test so the strict type checkers pass.
- Correct the durabletask sample catalog to the {executor}~{ordinal}~{requestId} qualified id format.
- Reword the Azure Functions sub-workflow sample intro so it does not imply a difference from a same-numbered sample.
- Drop internal shorthand (B2, phase labels) from code comments.

* fix(durabletask): reject case-insensitive workflow name collisions

The route ownership guard compares the durable orchestration name with casefold(), but registration kept raw names as distinct keys. Hosting 'Orders' and 'orders' therefore succeeded while either workflow's status/respond route could operate on the other's instances. Reject case-insensitive name collisions at registration (within a composition via collect_hosted_workflows, and across registration calls via the case-folded _registered_orchestrations map and the top-level guard in both hosts) so the case-folded ownership boundary stays real. Single names of any case remain valid; only collisions are rejected.

* docs(durabletask): remove multiworkflow/subworkflow ADR and design docs

Drop the ADR and design exploration documents and the dangling docstring reference to them.

* refactor(durabletask): simplify workflow client status parsing and drop deprecated orchestrator-name symbols

Extract a shared _parse_custom_status helper in DurableWorkflowClient to remove duplicated custom-status JSON parsing across three call sites.

Drop the now-unused single-workflow compatibility shims WORKFLOW_ORCHESTRATOR_NAME and WorkflowRegistrationPlan.orchestrator_name, replaced by per-workflow workflow_orchestrator_name(name).

* fix(core): drop WORKFLOW_ORCHESTRATOR_NAME from agent_framework.azure re-exports

The constant was removed from agent-framework-durabletask, but the core azure lazy-loading namespace still re-exported it, breaking pyright in packages/core. Remove it from both the runtime _IMPORTS map and the .pyi stub.

* fix(durabletask): atomic multi-workflow registration and bubble sub-workflow events

Make configure_workflow / AgentFunctionApp registration atomic: check every cross-call name collision before mutating any state, so a colliding nested sub-workflow no longer leaves a host partially configured (with the top-level name stuck in the registry). Applied to both the standalone worker and the Functions app.

Bubble sub-workflow intermediate events: a workflow run as a child orchestration now returns a SUBWORKFLOW_RESULT_KEY envelope carrying its outputs plus event timeline, and the parent re-tags the child's intermediate events with the WorkflowExecutor node id and republishes them, matching the in-process WorkflowExecutor contract. Top-level runs still return a bare outputs list.

Adds cross-registration atomicity tests on both hosts and unit tests for the result envelope and event bubbling. Resolves review threads on _worker.py, orchestrator.py, and test coverage.

* fix(azurefunctions): widen workflow orchestrator wrapper return type

The shared run_workflow_orchestrator now returns list | dict (the sub-workflow result envelope), so the azurefunctions _workflow.py wrapper that delegates to it must widen its Generator return annotation to match. Caught by the package-level pyright in CI (Package Checks), which type-checks the whole package, not just the files changed in the previous commit.
2026-07-07 14:43:28 +00:00
westey 757a832dbf .NET: Add OpenTelemetry Chat Client to harness stack (#6961)
* Add OpenTelemetry Chat Client to harness stack

* Address PR comments
2026-07-07 14:14:26 +00:00
Tamir Dresher cc20093da1 .NET: fix: bump GitHub.Copilot.SDK to 1.0.5 to resolve strong-naming mismatch (#6949)
* fix: bump GitHub.Copilot.SDK to 1.0.5 to resolve strong-naming mismatch

SDK 1.0.5 introduced strong-naming (PublicKeyToken=cc7b13ffcd2ddd51).
The adapter was compiled against the unsigned SDK (PublicKeyToken=null),
causing CS0012 for any consumer referencing both packages.

Fixes #6948

* fix: update tests and extension for SDK 1.0.5 namespace changes

- Add 'using GitHub.Copilot;' to CopilotClientExtensions.cs
- Change extension namespace to Microsoft.Agents.AI.GitHub.Copilot
- Update test files for new SDK types and removed APIs
- Add #pragma to suppress GHCP001 experimental warnings in tests
- All 45 tests pass across net8.0, net9.0, net10.0

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

* style: run dotnet format to fix linting issues

Remove unnecessary using directives (IDE0005) and fix file encoding (CHARSET).

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

* fix: revert CopilotClientExtensions namespace to GitHub.Copilot

Per reviewer feedback, extension methods should live in the namespace
of the type they extend (CopilotClient). This follows .NET team guidance.
The original namespace was GitHub.Copilot.SDK which was renamed to
GitHub.Copilot in SDK 1.0.5.

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

* refactor: narrow tools parameter from AITool to AIFunctionDeclaration

Since SessionConfig.Tools only accepts AIFunctionDeclaration, change the
constructor and extension method parameters to accept IList<AIFunctionDeclaration>
instead of the more general IList<AITool>. This makes the API honest about what
it actually uses and avoids silently discarding non-AIFunctionDeclaration tools.

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

* fix: sync Directory.Packages.props with upstream main

Take upstream's package versions (including MessagePack 3.1.7 pin
that fixes NU1902/NU1903 vulnerability warnings) while keeping
GitHub.Copilot.SDK at 1.0.5 which is the purpose of this PR.

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

---------

Co-authored-by: Tamir Dresher <tamirdresher@users.noreply.github.com>
Co-authored-by: Copilot <Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-07 14:09:28 +00:00
Giles Odigwe 9c4cd07899 Python: Add SkillsSourceContext to SkillsSource.get_skills (#6895)
* Python: Add SkillsSourceContext to SkillsSource.get_skills

Thread an invocation context (agent + optional session) through the skill
source pipeline so sources and decorators can make context-aware decisions.

- Add frozen, experimental SkillsSourceContext(agent, session).
- Change SkillsSource.get_skills and all sources/decorators to accept and
  forward the context.
- Make FilteringSkillsSource predicate context-aware: (skill, context) -> bool.
- Add optional cache_isolation_key_selector to CachingSkillsSource for
  per-key cache isolation (None keeps the shared-bucket behavior).
- Build the context in SkillsProvider from before_run agent/session.
- Update foundry_hosting toolbox source, exports, tests, and docs.

Python port of .NET PR #6797.

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

* Python: Clarify skills source docstring examples

Address PR review: docstring examples referenced `context` without
constructing it. Add a `SkillsSourceContext` construction line (with a
placeholder agent) to each source example and a note that the provider
normally supplies it. Use `source_context` in the FilteringSkillsSource
example to avoid clashing with the predicate's `context` parameter.

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

* Python: Fix CI type errors and skill_filtering sample predicate

Address CI failures from the SkillsSourceContext change:
- Update the skill_filtering sample to the 2-arg predicate signature
  (skill, context); the old 1-arg lambda would fail at runtime.
- Replace ad-hoc _StubAgent test stubs with the shared MockAgent /
  MockAgentSession from conftest so all type checkers (incl. ty) accept
  the SupportsAgentRun-typed agent. Add a small _NamedMockAgent subclass
  for tests needing distinct agent names, and drop now-unnecessary
  attr-defined ignores.
- Use cast(SupportsAgentRun, ...) in foundry_hosting tests, which have no
  shared mock infrastructure.

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

* Python: Make SkillsProvider caching safe-by-default; clarify context docstrings

Address PR review comments:
- Do not auto-wrap a caller-supplied SkillsSource in the provider's default
  CachingSkillsSource. A shared, unkeyed cache around a context-aware source
  replays the first invocation's skills for later SkillsSourceContexts,
  leaking skills across agents/tenants. Default caching now applies only to
  the built-in, context-independent file/in-memory leaf sources
  (Deduplicating(Caching(leaf))), matching the .NET provider. Callers who
  want caching on a custom pipeline compose CachingSkillsSource (optionally
  with a cache_isolation_key_selector) themselves. disable_caching now only
  affects the built-in leaves. Adds a leak-prevention test.
- Reword the misleading "Unused by this source" context docstrings on the
  File/InMemory/MCP sources: the param is part of the get_skills contract;
  these sources just return the same skills regardless of context.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-07 09:41:45 +00:00
westey cba9a1c050 Python: Add security information to harness features inline docs (#6936)
* Add security information to harness features inline docs

* Address PR comments

---------

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-07-07 09:33:44 +00:00
Evan Mattson c67372ff32 Python: [BREAKING]: Canonicalize AG-UI interrupt and resume handling (#6925)
* Python: Emit AG-UI interrupt outcomes

Key decisions: raise ag-ui-protocol to 0.1.19, type AGUIRequest/AGUIChatOptions with protocol Interrupt and ResumeEntry, and emit interrupted runs through RUN_FINISHED.outcome.interrupts instead of the legacy top-level interrupt field. Preserve existing internal resume/snapshot compatibility by translating legacy interruption metadata into canonical Interrupt metadata.

Files changed: packages/ag-ui pyproject, AG-UI run/type/workflow/snapshot helpers, AG-UI protocol-shape tests, and uv.lock.

Verification: uv run poe test -P ag-ui; uv run poe syntax -P ag-ui -C; uv run poe typing -P ag-ui; uv run poe validate-dependency-bounds-test -P ag-ui; uv run poe check -P ag-ui; git diff --cached --check.

Notes: README and local PRD/Ralph planning files were left unstaged. Follow-up slices still own richer approval/workflow response schemas and full client-side resume forwarding.

* Python: Emit canonical AG-UI approval interrupts

Key decisions: build Agent Framework approval pauses as canonical AG-UI Interrupt entries under RUN_FINISHED.outcome.interrupts; use reason=tool_call with toolCallId routing; advertise generic approval response schemas using the existing accepted/edited-argument payload contract; keep legacy Agent Framework approval metadata nested under metadata.agent_framework.value for internal snapshot/resume compatibility while avoiding any top-level interrupt value in emitted protocol JSON.

Files changed: packages/ag-ui/agent_framework_ag_ui/_run_common.py adds canonical approval interrupt/schema helpers and uses them for function approval requests; packages/ag-ui/agent_framework_ag_ui/_agent_run.py emits canonical interrupts for predictive confirm_changes pauses; packages/ag-ui/tests/ag_ui/test_endpoint.py covers endpoint/SSE approval pause shape; packages/ag-ui/tests/ag_ui/test_run.py covers helper and run-level confirmation interrupt behavior.

Verification: uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_pause_emits_canonical_interrupt_outcome packages/ag-ui/tests/ag_ui/test_run.py::test_emit_approval_request_populates_interrupt_metadata packages/ag-ui/tests/ag_ui/test_run.py::test_predictive_confirmation_run_finished_interrupt_links_tool_call -q; uv run pytest packages/ag-ui/tests/ag_ui/test_run.py::test_run_agent_stream_accumulates_multiple_confirm_interrupts packages/ag-ui/tests/ag_ui/test_endpoint.py::test_agent_endpoint_hydrates_interrupted_thread_without_invoking_agent packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_pause_emits_canonical_interrupt_outcome packages/ag-ui/tests/ag_ui/test_run.py::test_predictive_confirmation_run_finished_interrupt_links_tool_call -q; uv run poe test -P ag-ui; uv run poe syntax -P ag-ui -C; uv run poe pyright -P ag-ui; uv run poe typing -P ag-ui; uv run poe check -P ag-ui; git diff --check; git diff --cached --check.

Notes: local README, PRD, .ralph, and issue planning artifacts remain unstaged. Follow-up slices still own canonical ResumeEntry approval continuation, workflow request_info canonical resume, client-side forwarding, pending interrupt contract enforcement, snapshot stale-prompt clearing, and documentation/examples.

* Python: Resume AG-UI approvals canonically

Key decisions: translate canonical ResumeEntry approval payloads into the existing Agent Framework function approval response path at the AG-UI agent-run boundary; route by canonical interruptId while preserving pending approval registry validation; allow edited arguments only through canonical resume translation by updating the stored pending argument fingerprint before execution; emit RUN_ERROR for cancelled, unknown, or malformed approval resumes instead of proceeding.

Files changed: packages/ag-ui/agent_framework_ag_ui/_agent_run.py adds canonical approval resume translation, interrupt-id registry aliasing, explicit approval resume RUN_ERROR handling, and alias cleanup on consumption; packages/ag-ui/tests/ag_ui/test_endpoint.py adds endpoint/SSE coverage for approved, denied, edited, cancelled, and unknown canonical approval resumes.

Verification: uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_entry_executes_approved_tool packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_entry_denial_does_not_execute_tool packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_entry_applies_edited_arguments packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_cancelled_resume_entry_emits_run_error packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_unknown_resume_entry_emits_run_error -q; uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_pause_emits_canonical_interrupt_outcome packages/ag-ui/tests/ag_ui/test_endpoint.py::test_agent_endpoint_confirm_changes_clears_persisted_interrupt packages/ag-ui/tests/ag_ui/test_approval_result_event.py -q; uv run pytest packages/ag-ui/tests/ag_ui/test_agent_wrapper_comprehensive.py::test_approval_argument_mismatch_is_blocked packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_entry_applies_edited_arguments packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_entry_executes_approved_tool -q; uv run poe syntax -P ag-ui -C; uv run poe pyright -P ag-ui; uv run poe typing -P ag-ui; uv run poe test -P ag-ui; uv run poe check -P ag-ui; git diff --check; git diff --cached --check.

Notes: issues/agui-int-03-agent-approval-resume-entry.md was moved to issues/done locally but remains unstaged. Existing local README, PRD, and .ralph artifacts remain unstaged. Follow-up slices still own workflow request_info canonical resume, client-side forwarding, stricter pending interrupt contract enforcement, snapshot stale-prompt clearing, and documentation/examples.

* Python: Resume workflow interrupts canonically

Key decisions: emit workflow request_info pauses as canonical input_required interrupt outcomes with response schemas and Agent Framework metadata; normalize typed ResumeEntry model dumps through the shared resume parser while preserving status; translate resolved workflow resume payloads through the existing workflow response coercion path; emit RUN_ERROR for cancelled workflow resumes before invoking the workflow.

Files changed: packages/ag-ui/agent_framework_ag_ui/_run_common.py preserves canonical resume status/model dumps and merges interrupt metadata values; packages/ag-ui/agent_framework_ag_ui/_workflow_run.py builds canonical workflow request_info interrupts and cancellation errors; packages/ag-ui/tests/ag_ui/test_endpoint.py adds endpoint/SSE workflow pause, resolved resume, and cancelled resume coverage; packages/ag-ui/tests/ag_ui/test_run_common.py and packages/ag-ui/tests/ag_ui/test_workflow_run.py update canonical helper expectations.

Verification: uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_workflow_request_info_emits_canonical_interrupt_and_resumes packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_workflow_request_info_cancelled_resume_emits_run_error -q; uv run pytest packages/ag-ui/tests/ag_ui/test_workflow_run.py -q; uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_entry_executes_approved_tool packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_entry_denial_does_not_execute_tool packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_resume_entry_applies_edited_arguments packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_cancelled_resume_entry_emits_run_error packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_agent_approval_unknown_resume_entry_emits_run_error -q; uv run poe syntax -P ag-ui -C; uv run poe pyright -P ag-ui; uv run poe test -P ag-ui; uv run poe typing -P ag-ui; uv run poe check -P ag-ui; git diff --check; git diff --cached --check.

Notes: issue bookkeeping and local PRD files were not staged; existing unstaged packages/ag-ui/README.md remains untouched. Follow-up slices still own client-side forwarding, stricter pending interrupt contract enforcement, snapshot stale-prompt clearing, and documentation/examples.

* Python: Forward AG-UI interrupts through client

Key decisions: normalize typed Interrupt and ResumeEntry values at the AGUIChatClient and AGUIHttpService boundaries using protocol aliases; map legacy request_info available-interrupt hints to canonical input_required reason while preserving legacy resume wrapper shapes; preserve remote RUN_FINISHED.outcome metadata and expose outcome.interrupts for Agent Framework callers without changing normal success completion handling.

Files changed: packages/ag-ui/agent_framework_ag_ui/_client.py forwards normalized available_interrupts/resume values; packages/ag-ui/agent_framework_ag_ui/_http_service.py serializes typed protocol models and compatible values to camelCase wire JSON; packages/ag-ui/agent_framework_ag_ui/_event_converters.py preserves canonical outcomes/interruption metadata; packages/ag-ui/tests/ag_ui/test_ag_ui_client.py, test_http_service.py, and test_event_converters.py cover outgoing typed JSON, canonical interrupted conversion, and success outcome behavior.

Verification: uv run pytest packages/ag-ui/tests/ag_ui/test_http_service.py::test_post_run_serializes_typed_interrupts_and_resume_with_protocol_aliases packages/ag-ui/tests/ag_ui/test_ag_ui_client.py::TestAGUIChatClient::test_typed_interrupt_options_forward_canonical_protocol_shape packages/ag-ui/tests/ag_ui/test_event_converters.py::TestAGUIEventConverter::test_run_finished_event_with_canonical_interrupt_outcome packages/ag-ui/tests/ag_ui/test_event_converters.py::TestAGUIEventConverter::test_run_finished_event_with_success_outcome_preserves_normal_completion -q; uv run pytest packages/ag-ui/tests/ag_ui/test_http_service.py packages/ag-ui/tests/ag_ui/test_ag_ui_client.py packages/ag-ui/tests/ag_ui/test_event_converters.py -q; uv run poe syntax -P ag-ui -C; uv run poe pyright -P ag-ui; uv run poe test -P ag-ui; uv run poe typing -P ag-ui; uv run poe check -P ag-ui; git diff --check; git diff --cached --check.

Notes: issues/agui-int-05-chat-client-http-forwarding.md was moved to issues/done locally but not staged. Existing unstaged packages/ag-ui/README.md, .ralph, PRD, and snapshot planning artifacts remain untouched. Follow-up slices still own stricter pending interrupt contract enforcement, snapshot stale-prompt clearing, and documentation/examples.

* Python: Enforce AG-UI resume contract

Key decisions: validate pending AG-UI interrupts before agent or workflow execution; require resume entries to address every open interrupt exactly once; emit RUN_ERROR for missing, unknown, duplicate, malformed, cancelled, or schema-invalid resume payloads; keep successful canonical approval and workflow resume flows working while removing heuristic non-resume workflow continuation for interrupted threads.

Files changed: packages/ag-ui/agent_framework_ag_ui/_run_common.py adds strict resume parsing and exact pending-interrupt contract validation; _agent_run.py applies the contract to approval resumes, validates edited approval argument types, and considers stored canonical interrupt ids; _workflow_run.py applies the contract to request_info resumes and fails invalid response coercion explicitly; AG-UI endpoint, workflow, golden, wrapper, and subgraph tests now cover RUN_ERROR failures and canonical resume entries.

Verification: uv run pytest focused new resume-contract endpoint tests -q; uv run pytest existing approval/workflow resume endpoint tests -q; uv run pytest packages/ag-ui/tests/ag_ui/test_workflow_run.py packages/ag-ui/tests/ag_ui/test_run.py packages/ag-ui/tests/ag_ui/golden/test_scenario_workflow.py -q; uv run poe test -P ag-ui; uv run poe syntax -P ag-ui -C; uv run poe pyright -P ag-ui; uv run poe typing -P ag-ui; uv run poe check -P ag-ui; git diff --check; git diff --cached --check.

Notes: issues/agui-int-06-resume-contract-validation.md was moved to issues/done locally but not staged. Existing unstaged packages/ag-ui/README.md and local .ralph/PRD artifacts remain untouched. Follow-up slices still own canonical snapshot stale-prompt clearing and documentation/examples.

* Python: Clear AG-UI snapshot interrupts on cancel

Key decisions: treat cancelled canonical approval and workflow resumes as completion of the stored interruption for AG-UI Thread Snapshot hydration; clear only the persisted interrupt field while preserving replayable messages and Shared State; consume cancelled approval registry entries so server-side approval state does not remain open; preserve existing RUN_ERROR responses for cancelled resumes.

Files changed: packages/ag-ui/agent_framework_ag_ui/_snapshots.py adds shared persisted-interrupt clearing; _agent_run.py clears snapshots and consumes pending approvals on cancelled approval resumes; _workflow.py clears snapshots on cancelled workflow resumes; test_endpoint.py covers agent/workflow cancelled-resume stale prompt clearing; test_run_common.py covers canonical interrupt toolCallId trusted suffix filtering.

Verification: uv run pytest focused interrupted snapshot/resume tests -q; uv run poe test -P ag-ui; uv run poe syntax -P ag-ui -C; uv run poe pyright -P ag-ui; uv run poe typing -P ag-ui; uv run poe check -P ag-ui; git diff --check; git diff --cached --check.

Notes: issues/agui-int-07-thread-snapshot-interrupt-hydration.md was moved to issues/done locally but not staged. Existing unstaged packages/ag-ui/README.md and local .ralph/PRD artifacts remain untouched. Follow-up docs/examples slice still owns public guidance updates.

* Python: Document canonical AG-UI interrupts

Key decisions: document the clean release-candidate interrupt cutover around canonical AG-UI protocol models; direct users to RUN_FINISHED.outcome.interrupts and canonical resume arrays; make clear that Interrupt and ResumeEntry come from ag_ui.core rather than an Agent Framework-specific model; retain normal RUN_FINISHED completion guidance for non-interrupted runs.

Files changed: packages/ag-ui/AGENTS.md updates package guidance; packages/ag-ui/README.md adds interrupt/resume protocol and migration notes; packages/ag-ui/agent_framework_ag_ui_examples/README.md documents canonical resume shape for examples; packages/ag-ui/getting_started/README.md teaches outcome.interrupts and ResumeEntry usage.

Verification: uv run poe markdown-code-lint failed on pre-existing packages/mistral/README.md; uv run python scripts/check_md_code_blocks.py packages/ag-ui/README.md packages/ag-ui/agent_framework_ag_ui_examples/README.md packages/ag-ui/getting_started/README.md packages/ag-ui/AGENTS.md; git diff --check; git diff --cached --check.

Notes: no issue or PRD artifacts were staged. Root AGENTS.md could not be read because access was denied; package and Python workspace guidance were applied. Follow-up docs/examples issue appears complete; no remaining AG-UI interrupt cutover tasks were found locally.

* Canonicalize AG-UI interrupt and resume handling

* Fix AG-UI interrupt resume feedback

* Address AG-UI review feedback
2026-07-07 06:34:51 +00:00
Giles Odigwe bcd2800d4c Python: Allow disabling approval for SkillsProvider tools (#6867)
* Python: Allow disabling approval for SkillsProvider tools

Add disable_load_skill_approval, disable_read_skill_resource_approval, and disable_run_skill_script_approval keyword arguments to SkillsProvider.__init__ and SkillsProvider.from_paths. When set, the corresponding tool is registered with approval_mode=never_require so it runs without approval for trusted-skill scenarios. Approval remains required by default.

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

* Preserve from_paths compatibility for SkillsProvider subclasses

Forward the disable_*_approval kwargs from SkillsProvider.from_paths only when explicitly enabled, so subclasses that override __init__ with the previous signature keep working when the flags are left at their defaults. Add a regression test covering a legacy-signature subclass.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-07 04:25:49 +00:00
Evan Mattson 38b5f70d1f Fix README stars badge link (#6944) 2026-07-07 11:08:55 +09:00
Tao Chen 868744aeea Python: Process messages to an executor serially within a superstep (#6776)
* Process messages to an executor serially within a superstep

Add a per-executor asyncio.Lock in Executor.execute so each executor processes its messages one at a time within a superstep, while preserving concurrency across distinct executors. Includes a regression test.

* Create per-executor lock lazily under the running loop

asyncio.Lock created in Executor.__init__ would bind to the first event loop it was awaited under, so reusing an executor/workflow across loops (e.g. successive asyncio.run calls) raised 'bound to a different event loop'. Create the lock lazily via _get_execution_lock(), re-creating it when the running loop changes. Adds a loop-scoped lock test.

* Re-create runner context event queue lazily under the running loop

Like the per-executor lock, the runner context's asyncio.Queue bound to the first event loop it was awaited under, so reusing a workflow across loops (e.g. successive asyncio.run calls) raised 'bound to a different event loop'. Re-create the queue lazily via _get_event_queue() when the running loop changes. Adds an integration test reusing a workflow across event loops.

* Use lazy-None init for the event queue, matching the executor lock

Initialize _event_queue to None and create it on first use in _get_event_queue, mirroring the per-executor lock. Avoids constructing a queue in __init__/reset_for_new_run that is immediately discarded once the running loop is known.

* Improve comments

* Fix formatting
2026-07-06 22:40:48 +00:00
Tao Chen 5ac5038545 .NET: Add defense-in-depth for MCP cross origin request (#6871)
* Add defense-in-depth for MCP cross origin request

* Address comments
2026-07-06 17:37:36 +00:00
Tao Chen fc10ef31bd Python: Add FHA declarative workflow sample (#6897)
* Add FHA declarative workflow sample

* Address comments

* Address comments
2026-07-06 16:38:52 +00:00
Roger Barreto c09408cbd6 .NET: Fix flaky OpenTelemetryAgentTests via thread-safe activity collector (#6935)
* .NET: Fix flaky OpenTelemetryAgentTests via thread-safe activity collector

The Ctor_NullOrWhitespaceSourceName test subscribed a process-global TracerProvider to the shared default source Experimental.Microsoft.Agents.AI and exported into a plain List<Activity>. That source is also used by CompactionTelemetry, and xUnit runs the Compaction test classes in parallel, so a compaction span could be appended from another thread mid-assertion, throwing 'Collection was modified'.

Add a thread-safe ConcurrentActivityList collector (locked Add plus snapshot enumeration) for all InMemoryExporter collectors in the file, and scope the shared-source test to its own invoke_agent TraceId after ForceFlush so parallel compaction spans cannot affect the count or source-name checks.

* .NET: Assert ForceFlush result in OpenTelemetryAgentTests default-source test

Assert the boolean returned by TracerProvider.ForceFlush(timeout) so a flush timeout surfaces as a clear test failure instead of silently snapshotting incomplete activities.
2026-07-06 16:11:11 +00:00
dependabot[bot] 329d59eff4 Bump vite and @vitejs/plugin-react-swc (#6613)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react-swc). These dependencies needed to be updated together.

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

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

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
- dependency-name: "@vitejs/plugin-react-swc"
  dependency-version: 4.3.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-07-06 14:26:42 +09:00
dependabot[bot] a5fcd33967 Build(deps-dev): Bump js-yaml in /python/packages/devui/frontend (#6813)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.3.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 13:34:57 +09:00
westey ccba1fbbef .NET: [BREAKING] Align ShellPolicy allow/deny semantics with Python (#6906)
* Align dotnet shell policy with python implementation to support deny if not allowed semantics.

* Address PR comments.

* Address PR comments
2026-07-03 18:09:35 +00:00
Roger Barreto 2c7aadce4e .NET: Validate Foundry toolbox name is a single path segment before building the proxy URL (#6890)
* Validate Foundry toolbox name is a single path segment before building the proxy URL

Reject toolbox name/identifier inputs that carry path separators or relative-path segments (including their percent-encoded forms) before they are interpolated into the toolbox MCP proxy request URL, so a caller-influenced marker cannot alter the request target. Validation runs both at per-request marker resolution and at the shared open choke point, and is covered by red-to-green unit tests.

* Reject residual percent-encoding in toolbox name validation

After the bounded percent-decode loop, also reject a name that still contains a percent sign, so encoding nested deeper than the decode cap cannot survive validation. Dispose the service via await using in the rejection test. Adds a deeply-encoded coverage case.

* Validate toolbox name by request-target effect instead of a character list

Replace the character/decoding checks with an effect-based check: build the proxy URL and confirm the name resolves to a single, intact path segment between 'toolboxes' and 'mcp' with the scheme, authority, path shape, and fragment unchanged, and that the segment round-trips back to the name. This forgives characters that stay inside the segment (for example ':' , '@' , parentheses) while still rejecting names that would move the request target, including '?' and '#' and their percent-encoded forms. Adds coverage for the delimiter cases and for the newly-allowed names.
2026-07-03 17:00:26 +00:00
Roger Barreto 8be157e2a1 Return 404 for a response created against a nonexistent conversation (#6892)
When a create-response request references a conversation id that does not
exist, validate its existence up front and return a clean not-found error
mapped to HTTP 404, consistent with the Conversations API, instead of failing
mid-execution and surfacing a generic server error.

Centralize the responses validation error codes and their HTTP status mapping
in a single ResponseErrorCodes type so handlers translate a code to a 404 or
400 without ad-hoc string comparisons. Add unit and HTTP integration tests.
2026-07-03 17:00:00 +00:00
SergeyMenshykh 7ca73c0645 Update .NET version to 1.13.0 (#6900)
Co-authored-by: SergeyMenshykh <SergeMenshikh@outlook.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-03 16:30:21 +00:00
Eduard van Valkenburg 0260ea0e61 Python: implement ADR-0029 service_session_id lifecycle mapping (#6724)
* python: implement ADR-0029 service_session_id lifecycle mapping

- Extend AgentSession service_session_id to support structured values
- Add agent-owned conversation id extraction for chat forwarding and telemetry
- Migrate A2A durable continuation state to A2AServiceSessionId
- Keep A2AAgentSession as compatibility shim and mark it deprecated
- Update core/a2a tests and package guidance

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

* Fix service_session_id type fallout across packages

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

* Fix remaining test typing signatures for service_session_id

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

* Fix hosting test stubs for widened get_session type

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

* Fix remaining test stubs for get_session union type

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

* Simplify A2A session state handling

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

* fix import

* Fix hosting-telegram test get_session typing

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-02 18:36:48 +00:00
westey 24581d6865 Python: Allow devs to opt-out of file-access approvals (#6879) 2026-07-02 18:36:44 +00:00
SergeyMenshykh 331d17c5a1 .NET: fix: Require explicit TokenCredential in AddFoundryToolboxes (#6877)
* fix: require explicit TokenCredential in AddFoundryToolboxes

The AddFoundryToolboxes extension methods now require callers to
pass a TokenCredential explicitly rather than relying on an
internally-created default credential. This makes the credential
choice intentional and avoids non-deterministic credential probing
in production environments.

Breaking change (experimental API):
- AddFoundryToolboxes(IServiceCollection, params string[]) becomes
  AddFoundryToolboxes(IServiceCollection, TokenCredential, params string[])
- AddFoundryToolboxes(IServiceCollection, Action?, params string[]) becomes
  AddFoundryToolboxes(IServiceCollection, TokenCredential, Action?, params string[])
- Azure.Identity package dependency removed from Foundry.Hosting library.

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

* fix: simplify redundant generic type argument (IDE0001)

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

* fix: avoid duplicate FoundryToolboxService registration

Inject the AddFoundryToolboxes credential directly into the
FoundryToolboxService factory and fail early if the service was
already registered. This avoids registering TokenCredential in the
host DI container while preserving a single toolbox service instance
for both request handling and hosted startup.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-02 15:44:56 +00:00
Ben Thomas db80926f31 .NET: Improving DotNet samples (#6869)
* fix: resolve CA1873 in GitHubCopilotAgent by using LoggerMessage source generator

Replace the direct logger.LogWarning() call (which eagerly evaluates
string.Join()) with a [LoggerMessage]-generated extension method in
GitHubCopilotAgentLogMessages.cs.

Fixes build error:
  GitHubCopilotAgent.cs(580,13): error CA1873: Evaluation of this argument
  may be expensive and unnecessary if logging is disabled

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

* Fixing more dotnet samples

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Ben Thomas <25218250+alliscode@users.noreply.github.com>
2026-07-02 15:16:26 +00:00
westey 48436f8ab6 .NET: Make default-approval harness features configurable + customizable shell tool (#6880)
* Dotnet: Allow devs to opt-out of file-access approvals

* Address PR comments
2026-07-02 14:57:59 +00:00
Roger Barreto 551b44f04f .NET: Bump Azure.AI.Projects to 2.1.0-beta.4 (#6795)
* .NET: Bump Azure.AI.Projects to 2.1.0-alpha.20260629.1

Bumps Azure.AI.Projects beta.3 to alpha.20260629.1 and aligns transitive deps (System.ClientModel 1.14.0, Azure.Core 1.59.0, Msal 4.84.2). Adapts to renamed AgentSessionFiles APIs (Upload/GetAll/Delete, scoped GetAgentSessionFiles, SizeInBytes), AgentToolboxes (CreateVersion/Delete), and strongly typed toolbox tools (WebSearchToolboxTool, MCPToolboxTool). Adds azure-sdk public dev feed for prerelease restore.

* Use positional arg for AgentSessionFiles.DeleteAsync cleanup

* Move to Azure.AI.Projects 2.1.0-beta.4 (released beta)

Swaps the alpha daily build for the published 2.1.0-beta.4. Drops the azure-sdk public dev feed since beta.4 and its deps are on nuget.org. Beta.4 requires Azure.Core 1.60.0, which cascades the 10.0.8 servicing packages (Microsoft.Bcl.AsyncInterfaces, System.Diagnostics.DiagnosticSource, System.Text.Json, System.Threading.Channels, Microsoft.Extensions.DependencyInjection.Abstractions, Microsoft.Extensions.Logging.Abstractions) to 10.0.9.

* Reconcile Azure.Core 1.60.0 bump with merged main

Reverts the over-eager System.Threading.Channels 10.0.9 bump back to 10.0.8 (it was not part of the Azure.Core 1.60.0 cascade and caused a net472 MSB3277 conflict against the 10.0.8 that Microsoft.Extensions.AI pulls). Drops the now-obsolete Azure.Core VersionOverride=1.59.0 in HostedWorkflowHandoff (added on main to satisfy AgentServer while the central pin was lower); the central pin is now 1.60.0 which already satisfies the >=1.59.0 floor, and the override was downgrading this project below sibling projects (CS1705).
2026-07-02 14:40:03 +00:00
Eduard van Valkenburg 09ea690062 Python: Fix Hyperlight workspace staging (#6856)
* Fix Hyperlight workspace link staging

Reject symlinks, Windows junctions, and reparse points during Hyperlight input staging, and harden output collection/cleanup against the same link types.

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

* Address Hyperlight staging review

Anchor workspace enumeration to the resolved root and avoid following links while classifying output cleanup entries.

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

* Improve Hyperlight path resolve errors

Handle RuntimeError from path resolution alongside OSError when validating Hyperlight sandbox paths and report the source-root validation context in the error message.

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

* Mark Hyperlight real sandbox tests as integration

Ensure Windows unit CI excludes real Hyperlight sandbox tests by applying the integration marker consistently.

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

* Clean up Hyperlight integration sandboxes

Close real sandbox fixtures and provider-owned registries in Hyperlight integration tests so they do not rely on process teardown.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-02 14:02:38 +00:00
Roger Barreto 62f0024707 .NET: Foundry Hosting gracefully tolerates lacking user identity when run locally (#6870)
* .NET: Make Foundry Hosting resilient to missing user identity in local runs

AgentFrameworkResponseHandler threw InvalidOperationException (surfaced as a
500 on every request) when the isolation-key provider returned null, which
always happens locally because the platform x-agent-user-id header is absent.
Running a hosted image outside Foundry therefore failed out of the box.

The handler now branches on FoundryEnvironment.IsHosted: hosted stays strict
(null identity is still a hard error), but non-hosted (local docker run /
dotnet run) tolerates a null identity - per-user isolation is simply not
triggered, the request proceeds with userId null (no partition), and no
hosted context is stamped or validated.

Because local runs no longer need a fallback, the sample-side
DevTemporaryLocalUserIdProvider and AddDevTemporaryLocalContributorSetup are
removed from Hosted_Shared_Contributor_Setup and all sample Program.cs files.
To simulate distinct users locally, send an x-agent-user-id request header;
the default provider reads it exactly as it reads the platform-injected value.
The Memory sample smoke script now drives alice/bob against one container via
that header. AGENT_NAME defaults added to Hosted-ChatClientAgent and
Hosted-MemoryAgent so a hosted deploy (where AGENT_* is a reserved env var)
does not crash at startup.

Updates the two affected unit tests to assert the local-success path and
amends ADR 0031.

* Address review: correct isolation-guarantee and Memory-sample local docs

- AgentFrameworkResponseHandler: note the null/local case is unscoped/shared,
  not fully partitioned per user.
- HostedSessionIsolationKeyProvider XML docs: phrase the non-null UserId rule as
  a constraint on the returned-context case, since null is now allowed locally.
- Hosted-MemoryAgent: the PerUser() memory scope requires a resolved user, so a
  local run needs an x-agent-user-id header; corrected the Program.cs comment
  and README (removed the inaccurate "shared bucket locally" claim).
- Test: assert absence of any u-* per-user directory via a wildcard search
  rather than checking for a literal "u-" directory.
2026-07-02 09:32:22 +00:00
Eduard van Valkenburg e38592a23c Python: Fix Anthropic messages and function-loop fallback (#6794)
* Fix function loop fallback and Anthropic messages

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

* Address PR feedback for fallback and instructions

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-02 05:37:23 +00:00
Tao Chen 08a09e7ebb Python: Update FHA samples after v2 changes (#6841)
* Update FHA samples after v2 changes

* Add missing pakcage pin

* Address comments
2026-07-01 21:45:42 +00:00
SergeyMenshykh c1e20632f7 .NET: Remove Experimental attribute from Skills API in Microsoft.Agents.AI (#6861)
* .NET: Remove Experimental attribute from Skills API in Microsoft.Agents.AI

Closes microsoft/agent-framework#6835

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

* Restore MAAI001 suppression for Step07 sample (still uses ToolApproval experimental APIs)

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

* Keep bare NoWarn placeholder in Step01 and Step02 samples

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

---------

Co-authored-by: SergeyMenshykh <SergeMenshikh@outlook.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-01 17:33:10 +00:00
Giles Odigwe c41676682e Python: [BREAKING] Extract caching from SkillsProvider into a CachingSkillsSource decorator (#6847)
* Python: [BREAKING] Extract caching from SkillsProvider into CachingSkillsSource decorator

Adds a composable CachingSkillsSource(DelegatingSkillsSource) decorator that caches the inner source's skills list, and rewires SkillsProvider to wrap its resolved source in it by default (skipped when disable_caching=True). Removes the provider's baked-in caching (_cached_context field and _get_or_create_context). Mirrors .NET #6768.

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

* Add ty ignore for dynamic _test_context attribute in skills test helper

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-01 16:21:08 +00:00
Giles Odigwe effbd17325 Python: [BREAKING] Treat nested SKILL.md content as part of the parent skill (#6849)
* Python: Stop skill discovery at skill boundaries

File-based skill discovery kept descending after finding a SKILL.md, which treated content nested beneath a skill boundary as an independent skill root. Return immediately after recording a directory that contains SKILL.md so everything below it stays part of that skill, and add a regression test with a nested SKILL.md.

Fixes #6682

* Python: Attach nested skill content to the parent skill

Removing the SKILL.md subdirectory skip in resource and script scanning so that content beneath a skill boundary is attached to that skill, and update the discovery docstring and the nested-skill test to match. Complements the discovery early-return so a nested SKILL.md is never treated as an independent skill root.
2026-07-01 16:06:56 +00:00
westey bc8dd4b63c Python: [BREAKING] FileAccess/FileMemory replace_lines literal replacement with line deletion (#6859)
* FileAccess/FileMemory: Allowing removing lines by using full line replace

* Add agents.md changes

* Address PR comments
2026-07-01 15:44:11 +00:00
VectorPeak 0f1fa21070 Python: Accept A2A data URI media parameters (#6818)
Co-authored-by: VectorPeak <VectorPeak@users.noreply.github.com>
2026-07-01 15:27:53 +00:00
westey 300dfa7e36 .NET: [BREAKING] Refactor OpenAI Hosting OptionsMapping to disallow passing options by default (#6855)
* Refactor OptionsMapping to disallow passing options by default

* Address PR comments

* Address PR comment
2026-07-01 15:05:53 +00:00
westey e56f34c521 .NET: [BREAKING] Add file editing tools and align FileAccess/FileMemory store API (#6807)
* Add support for editing to file access and memory plus renames

* Address PR comments

* Address PR comments
2026-07-01 13:35:48 +00:00
SergeyMenshykh 51c05fc862 .NET: Add skill approval options (#6843)
* .NET: Add skill approval options

Add per-tool options for disabling approval on skills provider tools and cover the behavior with unit tests.

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

* .NET: Document mixed approval behavior

Document the non-approval bypass requirement and update tests to avoid file-discovery dependency.

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

---------

Co-authored-by: SergeyMenshykh <SergeMenshikh@outlook.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-01 12:46:08 +00:00
SergeyMenshykh b7fc23c61f .NET: Consolidate skill-source caching and make skill sources disposable (#6827)
* .NET: Consolidate skill-source caching and make skill sources disposable

Move all caching into the generic CachingAgentSkillsSource decorator and
remove the duplicate inline cache from AgentMcpSkillsSource, so a single
cache layer governs skill fetching. Add RefreshInterval-based expiry to
CachingAgentSkillsSourceOptions.

Make AgentSkillsSource (and its decorators) IDisposable so pipelines can
release owned resources, and give AgentSkillsProvider an ownsSource flag
controlling whether it disposes the source it wraps. Provider convenience
constructors and the builder set ownsSource: true.

Serialize ArchiveEntryLoader's reconcile/extract/read of the shared on-disk
directory with a per-instance lock to prevent concurrent corruption.

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

* .NET: Fix IDE0032 by using an auto-property in test source

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

* .NET: Make cancellation cache test deterministic

Ensure the first caller owns the fetch before the second caller queues, so
the cancellation-restart assertion is no longer race-prone.

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

* .NET: Throw ObjectDisposedException from CachingAgentSkillsSource after disposal

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

* .NET: Document AgentSkillsProviderBuilder source ownership and single-build contract

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

* .NET: Update API compatibility suppressions for AgentSkillsProvider ctor change

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

* .NET: Add test asserting archive skill updates are observed after reconcile

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

---------

Co-authored-by: SergeyMenshykh <SergeMenshikh@outlook.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-01 12:46:07 +00:00
SergeyMenshykh 00e4d4ffde Make skills source classes public and sealed with Experimental attribute (#6838)
- AgentInMemorySkillsSource: internal sealed → public sealed
- AggregatingAgentSkillsSource: internal sealed → public sealed
- CachingAgentSkillsSource: internal sealed → public sealed + [Experimental]
- DeduplicatingAgentSkillsSource: internal sealed partial → public sealed partial + [Experimental]
- FilteringAgentSkillsSource: internal sealed partial → public sealed partial + [Experimental]
- DelegatingAgentSkillsSource: internal abstract → public abstract + [Experimental]

Co-authored-by: SergeyMenshykh <SergeMenshikh@outlook.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-01 11:41:21 +00:00
Copilot 2cb97545bf .NET: Pin patched OpenAPI dependencies to unblock NU1903 in sample restores (#6853)
* Initial plan

* Bump OpenAPI packages to patched versions

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-01 11:25:58 +00:00
Giles Odigwe d50698bb79 Python: Allow custom argument parsing for skill scripts (#6817)
* Python: Allow custom argument marshaling for skill scripts

Add an optional argument_marshaler hook so callers can plug in their own argument conversion logic for inline skill scripts. Supplied at the InlineSkillScript, InlineSkill, and ClassSkill levels; when omitted, behavior is unchanged. This supports backends (e.g. vLLM) that send tool-call arguments in a non-conforming shape such as a JSON string.

Port of .NET PR #6498. Closes #6543.

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

* Address review feedback on skill argument marshaling

- Widen InlineSkillScript.run args to accept a raw str (the one place a marshaler-converted value is valid), and drop the now-unneeded type: ignore markers in tests.

- Constrain the SkillScriptArgumentMarshaler output type to dict | None so the type enforces the inline-script contract instead of a docstring note.

- Add a clear TypeError when a str reaches an inline script with no marshaler configured.

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

* Rename SkillScriptArgumentMarshaler to SkillScriptArgumentParser

In Python 'marshalling' specifically connotes the stdlib marshal module, so the term is misleading here. Rename the type alias, the argument_parser parameter/attribute, docstrings, exports, and tests accordingly.

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

* Fold argument_parser docstring into Args section

The skill constructors are fully keyword-only, so name/description/function are already documented under Args. Singling out argument_parser into its own Keyword Args section was inconsistent; merge it into Args for InlineSkillScript, InlineSkill, and ClassSkill.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-01 05:25:12 +00:00
Giles Odigwe 059e1e055f Python: Fix local history not injected when non-history context providers are present (#6810)
The auto-injection of InMemoryHistoryProvider was gated on there being no
context providers at all, so registering any non-history provider (e.g.
SkillsProvider, FileAccessProvider, or a RAG memory provider) suppressed local
history. On stateless clients this dropped prior messages across turns — most
visibly the tool-approval resume turn lost the prior assistant function_call,
causing a 400 "Expected toolResult blocks" error.

Gate the injection on the absence of a loading HistoryProvider instead, matching
the pattern already used in _workflows/_agent.py. Add regression tests covering
a non-history provider, an existing loading provider, and a persist-only
provider.

Fixes #5672

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-30 21:03:23 +00:00
Giles Odigwe 43f2095244 Python: Fix GeminiChatClient dropping image/file content (#6751)
* Python: Fix GeminiChatClient dropping image/file content

GeminiChatClient._convert_message_contents only handled text and function_call content, so data/uri (image, PDF, audio) parts were silently dropped and never reached Gemini. Convert data URIs to inline_data Parts and external URIs to file_data Parts, warning on genuinely unconvertible content. Adds tests for the multimodal conversion paths.

Fixes #6688

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

* Address review: strip data-URI mime params and handle non-inferable URIs

Strip parameters (e.g. charset) from a data URI media type before passing it to Gemini, and wrap types.Part.from_uri so a URI with no media_type and no guessable extension is passed through as file_data without a mime type instead of raising ValueError. Adds tests for both paths.

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

* Address review: reuse shared data-URI helpers

Reuse _get_data_bytes and detect_media_type_from_base64 from agent_framework instead of reimplementing base64 extraction/decoding and data-URI header parsing in the Gemini client. This also removes the manual header parsing that previously needed charset-parameter stripping. Updates tests accordingly.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-30 21:00:54 +00:00
Giles Odigwe f01bea77bb Python: remove hosting package entries from 1.10.0 CHANGELOG (#6846)
Hosting packages (hosting, hosting-responses, hosting-telegram) were excluded
from the 1.10.0 release but their entries remained in the CHANGELOG.
Also removes the core hosting channel entry since it's unreachable without
the hosting packages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-30 20:45:43 +00:00
Giles Odigwe a25756b9ec Python: selective version bump for 1.10.0 release (date 260630) (#6840)
- Re-date all beta/alpha packages to 260630 (actual release date)
- agent-framework-ag-ui: rc6 -> rc7 (fastapi bound update)
- agent-framework-github-copilot: rc1 -> rc2 (approval hook feature)
- Newly bumped: azure-ai-search, devui, gemini, hyperlight, ollama
- CHANGELOG [1.10.0] date updated to 2026-06-30 with new entries
- Hosting/hosting-responses/hosting-telegram excluded per team decision
- Inter-package dependency bounds updated
- uv.lock refreshed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-30 20:10:24 +00:00
Roger Barreto f9b2fbb676 .NET: Foundry Hosting per-user session isolation and Responses v2 protocol fast-fail (#6832)
* Add per-agent and per-user session storage isolation for Foundry Hosting

Partitions hosted session and checkpoint files as {root}/a-{agentName}/u-{userId}/c-{contextId}.json so a container that serves multiple agents and multiple users cannot leak state across tenants. The user layer collapses to a-{agent}/c-{conv}.json when no x-agent-user-id is present (raw local). Adds a reject-style path-traversal guard (CWE-22) for the untrusted user id plus a resolve-and-assert-under-root containment check, and keeps the strict-resume 403 identity check as a second defense layer.

AgentSessionStore.GetSessionAsync/SaveSessionAsync take a required (nullable) userId so a caller can never silently persist a session unscoped; the handler resolves the user id before loading the session and threads it to both. Tool approvals ride in the session checkpoint (ToolApprovalIdMap to AgentSessionStateBag), so the partitioned path covers them and no separate approval store is needed. Renames the sample HOSTED_USER_ISOLATION_KEY env var to HOSTED_USER_ID and DevTemporaryLocalSessionIsolationKeyProvider to DevTemporaryLocalUserIdProvider. Documents the design in ADR 0031. Adds handler-driven multi-agent/multi-user file-system tests and store-level traversal/isolation tests.

* Fail fast with a clear 501 when hosted container is served responses protocol 1.0.0

A 2.0.0-only hosted image served container protocol 1.0.0 (no x-agent-foundry-call-id
header) previously threw and surfaced an opaque 500 on every request. It now returns a
clear 501 "unsupported_container_protocol_version" naming the required protocol.

* HostedProtocolCompatibility gate keyed on FoundryEnvironment.IsHosted plus
  PlatformContext.CallId (the 2.0.0 exclusive marker); invoked before isolation resolution
* HostedProtocolCompatibilityTests unit coverage; AgentFrameworkResponseHandlerTests note
  clarifies the non-hosted path
* UnsupportedProtocolHostedAgentTests integration test deploys a dedicated
  it-unsupported-protocol agent as 1.0.0 and asserts the 501 (validated live on cace)
* TestContainer recognizes the unsupported-protocol scenario
* it-bootstrap-agents.ps1 placeholder default raised to responses 2.0.0 and adds the
  it-unsupported-protocol agent; HostedAgentFixture protocol version is overridable

* Address PR review: whitespace protocol gate and InMemory store agent keying

* HostedProtocolCompatibility treats a whitespace-only x-agent-foundry-call-id as
  absent (IsNullOrWhiteSpace) so a proxy injecting whitespace cannot bypass the gate;
  unit test covers empty, spaces and tab
* InMemoryAgentSessionStore keys sessions by agent.Name (omitting the agent segment
  when Name is unset), mirroring FileSystemAgentSessionStore, so session continuity
  survives a recreated or transient agent rather than keying on the per-instance agent.Id
2026-06-30 19:30:46 +00:00
Tao Chen 7f3a2aec38 [BREAKING] Python: Foundry Hosted Agent V2 protocol upgrade (#6811)
* Upgrade to FHA protocol v2 + toolbox integration

* Scope checkpoints and approval storage by user id

* Add toolbox skills integration

* Fix formatting

* Add httpx lower and upper bound

* Update foundry-hosting package version

* Remove custom http client

* Revert "Remove custom http client"

This reverts commit 60f1d5aa52.

* Remove custom http client

* correct fail fast exceptino wording
2026-06-30 17:13:08 +00:00
SergeyMenshykh 09fbccfb20 .NET: Add AgentSkillsSourceContext to AgentSkillsSource.GetSkillsAsync (#6797)
* Add AgentSkillsSourceContext to AgentSkillsSource.GetSkillsAsync

Pass agent/session context through the skills retrieval pipeline so
sources, filters, and caching can make context-aware decisions.

- AgentSkillsSourceContext (Agent, Session) is built by AgentSkillsProvider
  from the InvokingContext and flows through all sources and decorators.
- FilteringAgentSkillsSource predicate now receives an AgentSkillFilterContext
  bundling the skill and the source context.
- CachingAgentSkillsSource supports per-key isolation via
  CachingAgentSkillsSourceOptions.CacheIsolationKeySelector; a null selector
  preserves the shared-cache behavior.

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

* Make AgentSkillsSourceContext constructor public and harden cache key

- Make the AgentSkillsSourceContext constructor public so external callers
  can invoke AgentSkillsSource.GetSkillsAsync directly; drop the
  Mcp.UnitTests InternalsVisibleTo entry it required.
- Use a dedicated sentinel cache key for the shared bucket so an isolation
  selector returning an empty string gets its own bucket.
- Document cache-key cardinality guidance and baseline the experimental
  API breaking changes in CompatibilitySuppressions.xml.

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

* Drop AgentSkillFilterContext in favor of a two-argument filter predicate

Replace the AgentSkillFilterContext bundle with a
Func<AgentSkill, AgentSkillsSourceContext, bool> predicate in
FilteringAgentSkillsSource and AgentSkillsProviderBuilder.UseFilter, and
update the tests accordingly.

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

---------

Co-authored-by: SergeyMenshykh <SergeMenshikh@outlook.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-30 13:01:51 +01:00
Giles Odigwe 4cb1a6651d Python: Align GitHub Copilot provider function approval to use SDK on_pre_tool_use hook (#6750)
* Python: align GitHub Copilot approval to SDK on_pre_tool_use hook

Replace the bespoke on_function_approval enforcement in the GitHub Copilot provider with the Copilot SDK's native on_pre_tool_use hook. When no caller hook is supplied, a default hook returns 'ask' for approval_mode='always_require' tools (routed to on_permission_request) and defers others; a caller-supplied on_pre_tool_use takes precedence and logs a warning for any unenforced approval tool.

Fixes #6746

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

* Fix type-checker errors and restore load_dotenv in sample

Use a complete PreToolUseHookInput in on_pre_tool_use hook tests so pyright/pyrefly/ty/zuban no longer report missing required TypedDict keys. Restore load_dotenv() in the function-approval sample for consistency with the other GitHub Copilot samples (PR review feedback).

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

* Deprecate on_function_approval instead of removing it

Per PR review feedback, keep the on_function_approval callback working (still enforced in the tool handler for approval_mode='always_require' tools) but emit a DeprecationWarning at construction, so existing users get a signal rather than a silent behavior change. The default on_pre_tool_use ask-hook is not installed when on_function_approval is set, avoiding double-gating. Precedence: user on_pre_tool_use > on_function_approval > default ask-hook. Adds tests for the deprecated path and documents it in the package README.

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

* Make on_function_approval and on_pre_tool_use mutually exclusive

Per automated review feedback, instead of a precedence ordering between the deprecated on_function_approval callback and the new on_pre_tool_use hook (which silently double-gated when both were set), raise ValueError if both are supplied - at construction (both in default_options) or per run (per-run on_pre_tool_use with a construction-time on_function_approval). This matches the repo convention for deprecated-vs-new params (see _workflows/_workflow.py) and removes the flag-threading. Updates tests and the package README.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-30 11:40:59 +00:00
SergeyMenshykh 3cc511f0ca .NET: Harden dotnet-format workflow shell handling (#6796)
* Harden dotnet-format workflow shell handling

Minor robustness improvements to how the dotnet-format workflow passes values to the shell.

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

* Refine workflow path handling

Adjust shell settings around workflow path iteration for more predictable behavior.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-30 11:08:29 +00:00
westey a2f56f7688 Python: [BREAKING] Improve FileAccess/FileMemory harness providers (surgical edits, read-only tier, consistent naming) (#6801)
* Python: Add additional file access and memory provider functionality and renames for simplification and consistency

* Address PR comments and build failures.

* Address PR comments

* Address PR comments
2026-06-30 07:46:27 +00:00
749 changed files with 40455 additions and 26741 deletions
+4 -4
View File
@@ -42,7 +42,7 @@ jobs:
coreChanged: ${{ steps.filter.outputs.core }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: filter
with:
filters: |
@@ -610,12 +610,12 @@ jobs:
python-version: "3.13"
os: ${{ runner.os }}
- name: Download all test results from current run
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: dotnet-test-results-*
path: dotnet-test-results/
- name: Restore report history cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: python/dotnet-integration-report-history.json
key: dotnet-integration-report-history-${{ github.run_id }}
@@ -632,7 +632,7 @@ jobs:
run: cat dotnet-integration-test-report.md >> $GITHUB_STEP_SUMMARY
- name: Save report history cache
if: always()
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: python/dotnet-integration-report-history.json
key: dotnet-integration-report-history-${{ github.run_id }}
+10 -3
View File
@@ -54,11 +54,14 @@ jobs:
- name: Find csproj files
id: find-csproj
if: github.event_name != 'pull_request' || steps.changed-files.outputs.added_modified != '' || steps.changed-files.outcome == 'failure'
env:
ADDED_MODIFIED: ${{ steps.changed-files.outputs.added_modified }}
run: |
csproj_files=()
exclude_files=("Experimental.Orchestration.Flow.csproj" "Experimental.Orchestration.Flow.UnitTests.csproj" "Experimental.Orchestration.Flow.IntegrationTests.csproj")
set -f
if [[ ${{ steps.changed-files.outcome }} == 'success' ]]; then
for file in ${{ steps.changed-files.outputs.added_modified }}; do
for file in $ADDED_MODIFIED; do
echo "$file was changed"
dir="./$file"
while [[ $dir != "." && $dir != "/" && $dir != $GITHUB_WORKSPACE ]]; do
@@ -80,6 +83,7 @@ jobs:
csproj_files=($(printf "%s\n" "${csproj_files[@]}" | sort -u))
echo "Found ${#csproj_files[@]} unique csproj/slnx files: ${csproj_files[*]}"
echo "csproj_files=${csproj_files[*]}" >> $GITHUB_OUTPUT
set +f
- name: Pull container dotnet/sdk:${{ matrix.dotnet }}
if: steps.find-csproj.outputs.csproj_files != ''
@@ -88,8 +92,11 @@ jobs:
# This step will run dotnet format on each of the unique csproj files and fail if any changes are made
- name: Run dotnet format
if: steps.find-csproj.outputs.csproj_files != ''
env:
CSPROJ_FILES: ${{ steps.find-csproj.outputs.csproj_files }}
run: |
for csproj in ${{ steps.find-csproj.outputs.csproj_files }}; do
set -f
for csproj in $CSPROJ_FILES; do
echo "Running dotnet format on $csproj"
docker run --rm -v $(pwd):/app -w /app mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet }} /bin/sh -c "dotnet format $csproj --verify-no-changes --verbosity diagnostic"
docker run --rm -v "$(pwd):/app" -w /app mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet }} dotnet format "$csproj" --verify-no-changes --verbosity diagnostic
done
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
with:
path: ~/.cache/prek
key: prek|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
- uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # v1
- uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
name: Run Pre-commit Hooks (excluding poe-check)
env:
SKIP: poe-check
@@ -177,7 +177,7 @@ jobs:
run: curl -fsSL https://ollama.com/install.sh | sh
working-directory: .
- name: Cache Ollama models
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.ollama/models
key: ollama-models-qwen2.5-1.5b-nomic-embed-text-v1
@@ -546,12 +546,12 @@ jobs:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Download all test results from current run
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: test-results-*
path: test-results/
- name: Restore report history cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: python/integration-report-history.json
key: integration-report-history-integration-${{ github.run_id }}
@@ -568,7 +568,7 @@ jobs:
run: cat integration-test-report.md >> $GITHUB_STEP_SUMMARY
- name: Save report history cache
if: always()
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: python/integration-report-history.json
key: integration-report-history-integration-${{ github.run_id }}
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
pythonChanges: ${{ steps.filter.outputs.python}}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: filter
with:
filters: |
+5 -5
View File
@@ -43,7 +43,7 @@ jobs:
githubCopilotChanged: ${{ steps.filter.outputs.github_copilot }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: filter
with:
filters: |
@@ -298,7 +298,7 @@ jobs:
run: curl -fsSL https://ollama.com/install.sh | sh
working-directory: .
- name: Cache Ollama models
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.ollama/models
key: ollama-models-qwen2.5-1.5b-nomic-embed-text-v1
@@ -743,12 +743,12 @@ jobs:
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
- name: Download all test results from current run
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: test-results-*
path: test-results/
- name: Restore report history cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: python/integration-report-history.json
key: integration-report-history-merge-${{ github.run_id }}
@@ -765,7 +765,7 @@ jobs:
run: cat integration-test-report.md >> $GITHUB_STEP_SUMMARY
- name: Save report history cache
if: always()
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: python/integration-report-history.json
key: integration-report-history-merge-${{ github.run_id }}
@@ -693,7 +693,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Download all validation reports
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: validation-report-*
path: reports/
@@ -701,7 +701,7 @@ jobs:
- name: Restore validation history
id: cache-restore
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: validation-history/
key: validation-history-${{ github.run_id }}
@@ -719,7 +719,7 @@ jobs:
run: cat trend-report.md >> "$GITHUB_STEP_SUMMARY"
- name: Save validation history
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: validation-history/
key: validation-history-${{ github.run_id }}
@@ -57,7 +57,7 @@ jobs:
echo "PR_NUMBER=$ARTIFACT_PR_NUMBER" >> "$GITHUB_ENV"
- name: Pytest coverage comment
id: coverageComment
uses: MishaKav/pytest-coverage-comment@26f986d2599c288bb62f623d29c2da98609e9cd4 # v1.6.0
uses: MishaKav/pytest-coverage-comment@dd5b80bde6d16941f336518e92929e89069d8451 # v1.7.2
with:
github-token: ${{ github.token }}
issue-number: ${{ env.PR_NUMBER }}
+1 -1
View File
@@ -6,7 +6,7 @@
[![MS Learn Documentation](https://img.shields.io/badge/MS%20Learn-Documentation-blue)](https://learn.microsoft.com/en-us/agent-framework/)
[![PyPI](https://img.shields.io/pypi/v/agent-framework)](https://pypi.org/project/agent-framework/)
[![NuGet](https://img.shields.io/nuget/v/Microsoft.Agents.AI)](https://www.nuget.org/profiles/MicrosoftAgentFramework/)
[![GitHub stars](https://img.shields.io/github/stars/microsoft/agent-framework?style=social)](https://github.com/microsoft/agent-framework/stargazers)
[![GitHub stars](https://img.shields.io/github/stars/microsoft/agent-framework?style=social)](https://github.com/microsoft/agent-framework)
Microsoft Agent Framework (MAF) is an open, multi-language framework for building **production-grade AI agents and multi-agent workflows** in **.NET and Python**.
+461 -87
View File
@@ -1,144 +1,518 @@
---
status: accepted
contact: eavanvalkenburg
date: 2026-06-11
date: 2026-06-30
deciders: eavanvalkenburg
consulted: rogerbarreto, moonbox3
---
# Python minimal hosting core and pluggable channels
# Python protocol helpers and optional execution state
## Context and Problem Statement
Agent Framework has several protocol-specific hosting surfaces. App authors who want one agent or workflow on multiple protocols must compose servers, routes, middleware, session handling, and lifecycle code by hand.
Agent Framework needs to help applications expose agents and workflows over external protocols such as OpenAI
Responses, Telegram, Activity Protocol, and future transports.
We will introduce a small Python hosting core that owns the common server shape and leaves protocol details inside channel packages. The first public contract must be intentionally narrow so Python can ship a base contract before adding identity linking, proactive delivery, or multicast behavior. Other language implementations may reuse the same conceptual boundary, but this ADR records the Python decision.
FastAPI, Starlette, Azure Functions, Django, Telegram SDKs, Bot Framework SDKs, and other app frameworks already own
route registration, dependency injection, middleware, authentication, background tasks, lifecycle, and native client
calls. Agent Framework should not duplicate those surfaces unless a specific hosting environment requires it.
## Decision Drivers
- Keep the first host easy to explain: one app, one hostable target, one or more channels.
- Reuse Agent Framework's existing agent, workflow, session, history, and checkpoint primitives.
- Let channel packages own protocol parsing, protocol responses, authentication details, and native command surfaces.
- Make session continuity explicit through a channel-supplied `ChannelSession(isolation_key=...)`.
- Avoid approving cross-channel identity and delivery semantics before their safety model is reviewed.
- Keep the released surface small enough to explain without first teaching a channel framework.
- Provide reusable Agent Framework run translation that works with FastAPI, Django, and other web frameworks.
- Let app/framework code own route declaration, auth, middleware, native SDK clients, command handling, and background
work.
- Keep stateful execution support explicit: session lookup/storage and workflow checkpoint lookup/storage may still need
a small AF-owned home.
## Considered Options
1. Keep only protocol-specific hosts.
2. Ship a large hosting core with identity linking, authorization, background delivery, active-channel routing, and multicast in v1.
3. Ship a minimal host/channel core now and track linking/multicast as follow-up work.
1. Create protocol-specific hosts.
2. Ship a full host/channel framework with route contribution and channel hooks.
3. Ship protocol conversion helpers plus optional execution state.
### Keep only protocol-specific hosts
### 1. Create protocol-specific hosts
- Good: no new abstraction or package surface.
- Neutral: each protocol can continue evolving independently.
- Bad: every multi-channel app still has to compose servers, lifecycle, and session handling by hand.
- Good: no new shared abstraction.
- Neutral: each protocol host can evolve independently.
- Bad: every package reinvents AF input/result mapping, session-key conventions, and stateful execution helpers.
### Ship the large cross-channel host in v1
### 2. Ship a full host/channel framework
- Good: the richest cross-channel scenarios are available immediately.
- Neutral: the host becomes the natural place to demonstrate identity and delivery policy.
- Bad: v1 becomes a security-sensitive identity and delivery system before the safety model is reviewed.
- Good: one object can assemble routes, channels, session handling, hooks, and lifecycle callbacks.
- Good: app code using the supported host shape can be short.
- Bad: the framework owns concerns already handled by web frameworks, protocol SDKs and/or other services.
- Bad: users must understand `Channel`, contribution, hook, and host-dispatch concepts before they can see how a request
becomes `agent.run(...)`.
- Bad: the abstraction is hard to reuse outside the chosen web framework.
### Ship the minimal core now
### 3. Ship protocol helpers plus optional execution state
- Good: the host/channel boundary can be implemented, tested, and explained without solving linking and durable delivery at the same time.
- Neutral: apps that need richer behavior must build it locally or wait for ADR-0028 follow-up work.
- Bad: proactive delivery and multicast scenarios are deliberately absent from v1.
- Good: protocol packages provide the Agent Framework run value directly: `<protocol>_to_run(...)` and
`<protocol>_from_run(...)` style helpers.
- Good: apps keep native FastAPI, Starlette, Azure Functions, Django, Bot Framework, or Telegram SDK code.
- Good: helper functions can be tested without a web framework app or host pipeline.
- Good: small state objects can still own target-coupled state: `AgentState` pairs an agent target with a `SessionStore`,
and `WorkflowState` resolves a workflow target while reusing the existing `CheckpointStorage` abstraction.
- Good: provides maximum configurability in handling input and outputs (outside of the conversions)
- Bad: building a first iteration of a new Host is more verbose.
- Bad: samples show more explicit route/client code than a fully assembled channel host.
## Decision Outcome
Chosen option: **minimal host/channel core now, follow-up enhancements later**.
Chosen option: **3. Ship protocol helpers plus optional execution state**.
`AgentFrameworkHost` owns:
Protocol packages own:
- one application object,
- one hostable target (`SupportsAgentRun` agent-compatible object or a `Workflow`), and
- one or more channels.
- parsing protocol-native input into Agent Framework run input and options;
- rendering `AgentResponse`, `AgentResponseUpdate`, workflow results, or workflow updates back into protocol-native
response/event payloads;
- protocol-specific isolation/session id helper functions when useful, such as `telegram_session_id(update)`;
- protocol-specific typing/update event helpers where the protocol has a native concept.
Channels own:
Application or web-framework code owns:
- contributed routes, middleware, commands, and lifecycle callbacks,
- protocol-native request parsing into `ChannelRequest`,
- protocol-native rendering of the originating response, and
- any channel-specific authentication or signature validation.
- HTTP route declaration and route grouping;
- dependency injection;
- authentication and authorization;
- middleware;
- background tasks and webhook acknowledgement policy;
- native protocol SDK clients and outbound calls;
- command registration and command dispatch;
- request/response status codes and framework-specific error handling;
- choosing the isolation/session id source for the current deployment and route.
The host owns:
The application builder can make the server exactly as they see fit, but this is outside the responsibilities of this proposed scheme.
This might include implementing other known API surfaces from vendors like OpenAI, such as creating conversations, vector stores, deleting things, etc.
If they want they can build the full OpenAI API, but it will include code that does not rely on agent-framework-hosting, which is fine.
They are responsible for what they expose.
- route/lifecycle aggregation,
- invocation of the target,
- `ChannelSession(isolation_key=...)` to `AgentSession` resolution and caching,
- `reset_session(isolation_key=...)`,
- host-level middleware, including Foundry isolation middleware only when the Foundry hosting environment flag is present,
- invocation of per-channel hooks (`ChannelRunHook`, `ChannelResponseHook`, `ChannelStreamUpdateHook`), and
- workflow checkpoint wiring through an explicit `checkpoint_location`.
The optional execution-state helpers, if provided, are limited to shared execution state:
`ChannelIdentity`, when present, is request metadata only. In v1 it is not a linking, authorization, or delivery key.
- `AgentState`: one `SupportsAgentRun`-compatible target plus a `SessionStore`;
- `WorkflowState`: one `Workflow`, `WorkflowBuilder`-shaped builder, orchestration builder, or workflow factory;
- `SessionStore`: plain async storage (`get` / `set` / `delete`) by an app-selected id.
### Trust boundary for `isolation_key`
The store does not create sessions. `AgentState` provides the target-aware `get_or_create_session(...)` helper because
only the state object has both the store and the resolved agent target. Workflow checkpointing should use the existing
`CheckpointStorage` abstraction directly; app/state code may keep a small cursor (`session_id -> checkpoint_id`) when it
needs to resume a workflow for a session.
The host treats `ChannelSession.isolation_key` as a session partition key, not as proof of identity. Channels or host middleware must authenticate and authorize any externally supplied value before passing it to the host. For example, a Responses caller must not be allowed to choose an arbitrary `previous_response_id` or header-derived key unless the platform or middleware has already established that the caller owns that conversation. The host deliberately does not infer that trust from the string itself.
These objects are **not** app objects, channel registries, or route owners. They do not own FastAPI/Starlette setup,
route contribution, protocol dispatch, command projection, or native SDK calls.
### Hook ownership
### Helper naming and families
Channels provide hook configuration and protocol-native context. The host invokes those hooks as part of the common invocation pipeline:
Helpers should be protocol-specific, not generic. Avoid a generic `protocol_to_run(...)` name in public samples because it
hides the protocol-specific contract behind a second abstraction.
- `ChannelRunHook` runs after channel parsing and before target invocation.
- `ChannelResponseHook` runs after target invocation and before the originating channel serializes its response.
- `ChannelStreamUpdateHook` is applied by the host while the channel consumes streamed updates because streaming serialization is protocol-specific.
Protocol packages should consider these helper families. This table is a set of examples, not a required protocol or
checklist. Not every protocol needs every helper, but when a protocol has the concept the naming should stay consistent:
`ChannelStreamUpdateHook` is an update hook, not a final-response sanitizer. Channels that use it for redaction or filtering must also apply equivalent policy to any final response they render. Channels choose whether the response is streaming before run hooks execute.
| Helper family | Shape | Purpose |
| --- | --- | --- |
| Run conversion | `<protocol>_to_run(...)` | Convert one protocol-native call/update/request into `Agent.run` or `Workflow.run` values. |
| Final rendering | `<protocol>_from_run(...)` | Convert a final `AgentResponse` / workflow result into protocol-native response payloads or operations. |
| Stream rendering | `<protocol>_from_streaming_run(...)` | Convert `ResponseStream` / workflow updates into protocol-native events or operations. |
| Session id extraction | `<protocol>_session_id(...)` | Extract the protocol's natural continuation/partition key from the call, if present. |
| Command/action parsing | `<protocol>_command(...)` | Parse a protocol-native command/action/operation name without deciding app policy. |
This keeps hook call conventions centralized while leaving protocol payload parsing and response formatting in channel packages.
Examples:
### State owned by v1
- `responses_to_run(...)`, `responses_from_run(...)`, `responses_from_streaming_run(...)`,
`responses_session_id(...)`;
- `telegram_to_run(...)`, `telegram_from_run(...)`, `telegram_from_streaming_run(...)`,
`telegram_session_id(...)`, `telegram_command(...)`;
- `activity_to_run(...)`, `activity_from_run(...)`, `activity_session_id(...)`, `activity_command(...)`;
- `discord_to_run(...)`, `discord_from_run(...)`, `discord_session_id(...)`, `discord_command(...)`.
`state_dir` is limited to host-owned local files for reset-session aliases and workflow checkpoint path derivation. It does not store linked identities, active-channel state, response-routing state, continuation records, durable runner queues, or delivery attempts. Those storage concerns belong to ADR-0028.
The app still owns what a parsed command means. For example, a Telegram `/new`, Discord slash command, Bot Framework
command activity, or A2A cancellation/request action may parse through a command/action helper, but the route or SDK
handler decides whether that command clears a session, cancels a task, calls an agent, or is ignored.
Additional helper functions can be protocol-specific when the concept is not broadly shared. Examples include
`telegram_chat_id(...)`, `telegram_callback_query_id(...)`, `telegram_media_file_id(...)`,
`discord_interaction_id(...)`, `a2a_task_id(...)`, `a2a_context_id(...)`, and MCP tool/prompt/resource helpers. These
helpers should still stay side-effect-free: they extract, normalize, or describe protocol data, while app/native SDK code
performs acknowledgements, sends/edits messages, resolves protected file URLs, applies rate limits, and registers
handlers.
### Security responsibilities for application builders
The application builder owns the trust boundary. Protocol helper packages can parse native payloads and expose candidate
ids or operations, but they do not authenticate callers, authorize access to state, or decide which side effects are
allowed.
Application code that uses these helpers are responsible for (this means that we advice you to think through these topics,
but ultimately, the choice of which controls are needed for the intended use case is up to the application builder):
- authenticate the caller through the app's normal mechanism before using protocol-provided ids;
- authorize any caller-supplied session, checkpoint, task, context, conversation, thread, or response id before loading
state for it;
- bind externally supplied ids to the authenticated user, tenant, workspace, installation, or chat context before using
them as `SessionStore` keys or checkpoint cursor keys;
- treat `<protocol>_session_id(...)` results as untrusted candidate keys until that ownership check has passed;
- keep platform-provided isolation helpers fail-closed outside their trusted hosting environment;
- authorize command/action effects such as reset, cancel, approve, submit, or tool invocation after parsing them;
- opt in explicitly before resolving protected media/resource/file URLs and passing them to a remote model provider;
- persist post-run session or checkpoint state only after `agent.run(...)`, `workflow.run(...)`, or stream finalization has
updated that state.
For Foundry specifically, helpers may read values established by Foundry hosting middleware, but must not treat raw
request headers as trusted Foundry isolation when the app is running outside Foundry. Implementations must test that
non-Foundry requests do not accept spoofable isolation headers as platform-provided keys.
For workflow checkpointing, the checkpoint boundary must be at least as specific as the authorized session/tenant
boundary. A shared storage lookup such as "latest checkpoint for workflow name" is safe only when the storage is already
scoped to the authorized session. In a shared durable store, map the authorized `session_id` to a checkpoint id or other
cursor and load that specific checkpoint.
### Session continuity
Session continuity remains explicit. Run parsing and isolation/session id selection are separate operations because
isolation can come from more than one source:
- protocol input, such as OpenAI Responses `previous_response_id`, a Telegram chat id, or an Activity conversation id;
- running environment, such as Foundry Hosted Agents user/chat isolation context;
- app-specific trusted middleware or route state.
The app chooses which helper to call for that route and deployment. For example:
- `responses_session_id(body)` from `agent-framework-hosting-responses`, which can return either a `resp_*` previous
response id or a `conv_*` conversation id when present;
- `telegram_session_id(update)` from `agent-framework-hosting-telegram`, which can choose the chat, user, thread, or
other Telegram-native partitioning logic for that helper;
- `activity_session_id(activity)`, `discord_session_id(interaction_or_message)`, or
`a2a_session_id(request_context)` from their respective protocol packages;
- `foundry_user_isolation_key()` or `foundry_chat_isolation_key()` from `agent-framework-foundry-hosting`.
Keep these helpers outside `responses_to_run(...)`, `telegram_to_run(...)`, and other run-input parsers. That makes the
trust boundary visible: using a request-derived key is a different decision than using a platform-provided isolation key.
The application builder is also responsible for deciding whether the hosting environment is **persistent** (for example,
a long-running container or web app) or **transient** (for example, Azure Functions, Foundry Hosted Agents, or any
environment where process memory is not a reliable continuity boundary). That decision controls which state mechanisms are
safe to use:
- persistent single-process apps may use in-memory state for local development or simple deployments, while still needing
durable state for multi-replica continuity;
- transient apps must not rely on in-memory `SessionStore` state between calls and need a durable session store or a
service-owned continuation id;
- workflow hosts must choose an explicit `CheckpointStorage` and, when they need per-session resume, a durable
`session_id -> checkpoint_id` cursor because in-process workflow state and in-memory checkpoint cursors do not survive
transient execution.
A `SessionStore` stores `session_id -> AgentSession`, but it does not create sessions. `AgentState` resolves the agent
target and creates the session on first use:
For agent targets:
```python
session = await state.get_or_create_session(session_id)
target = await state.get_target()
result = await target.run(messages, session=session, options=options)
```
If the protocol mints a new continuation id as part of the response being created (for example, OpenAI Responses
`resp_*` ids), store the **post-run** session explicitly under that new id:
```python
session = await state.get_or_create_session(previous_response_id)
target = await state.get_target()
result = await target.run(messages, session=session, options=options)
await state.set_session(response_id, session)
```
`agent.run(...)` may update the session object (for example, with service continuation state), so the explicit store call
belongs after the run, not before it.
The session id is a partition key, not proof of identity. App or platform code must authenticate and authorize any
externally supplied key before using it.
### Workflow checkpoints
Workflow checkpointing is execution state, not protocol state. `WorkflowState` pairs a workflow target with checkpoint
state, but it should not wrap or replace the existing `CheckpointStorage` abstraction. Apps should pass the actual
`CheckpointStorage` they want the workflow to use. If an app needs per-session resume, it can keep a small cursor from
authorized `session_id` to `checkpoint_id` (or an equivalent store-specific resume token).
Workflow runs do not currently emit a checkpoint id on `WorkflowRunResult` or normal workflow events by default. The
runner receives checkpoint ids internally from `CheckpointStorage.save(...)`. App/state code that owns the storage can
observe the latest id by querying the storage after a run, for example
`await storage.get_latest(workflow_name=target.name)`.
For workflow targets, app code adapts the protocol helper output into the workflow's expected input and invokes the
workflow through the state object's target:
```python
# session_id must already be authenticated and authorized for this caller
target = await state.get_target()
result = await target.run(message=workflow_input, checkpoint_storage=checkpoint_storage)
latest = await checkpoint_storage.get_latest(workflow_name=target.name)
if latest is not None:
await checkpoint_cursor_store.set(session_id, latest.checkpoint_id)
```
If a route wants to resume from a prior checkpoint, it explicitly chooses the checkpoint and passes it to
`workflow.run(...)`:
```python
# session_id must already be authenticated and authorized for this caller
target = await state.get_target()
checkpoint_id = await checkpoint_cursor_store.get(session_id)
if checkpoint_id is None:
result = await target.run(message=workflow_input, checkpoint_storage=checkpoint_storage)
else:
result = await target.run(checkpoint_id=checkpoint_id, checkpoint_storage=checkpoint_storage)
latest = await checkpoint_storage.get_latest(workflow_name=target.name)
if latest is not None:
await checkpoint_cursor_store.set(session_id, latest.checkpoint_id)
```
`workflow.run(...)` writes checkpoints to the provided storage, so storage selection must be explicit at the route layer.
Protocol helper packages should not own checkpoint layout, route lifecycle, or durable execution.
## Non-goals for v1
The following are deliberately **not** part of the v1 contract:
The following remain outside the v1 protocol-helper contract. Some are deliberately app-owned in v1; others are possible
future framework work only after a separate design.
- cross-channel identity linking (`IdentityLinker`, `local_identity_link`, or `agent-framework-hosting-entra`),
- identity allowlists or authorization policy (`IdentityAllowlist`, `AuthPolicy`),
- response routing beyond the originating channel (`ResponseTarget`, active channel, specific linked channel, `all_linked`),
- push or payload codecs (`ChannelPush`, `ChannelPushCodec`),
- background/continuation delivery,
- durable task runners (`DurableTaskRunner`, `InProcessTaskRunner`),
- retry/replay policy (`RetryPolicy`),
- fan-out, multicast, or all-linked delivery,
- confidentiality tiers and `LinkPolicy`, and
- a host-level multi-agent router.
### App-owned in v1
These areas are follow-up enhancements covered by [ADR-0028](0028-hosting-linking-multicast-enhancements.md). They are not prerequisites for shipping or using the v1 host.
The app builder owns these concerns with normal web-framework, SDK, platform, or application code:
- authentication, authorization policy, and allowlists;
- deciding whether identities across protocols map to the same `session_id`;
- non-originating sends using native SDK clients;
- background work, durable execution, retry, and replay when app code owns the work;
- routing between multiple agents.
This is easier in the protocol-helper model than it was in the host/channel model: app code already owns the native SDK
clients, route handlers, authenticated caller context, session id selection, and outbound send calls. An app can link
channels by choosing the same authorized `session_id` for multiple protocols, and can do non-originating delivery by
calling the destination protocol's native client directly. That does not make a reusable framework feature safe by
default; it just means the app-specific version no longer has to fight a host abstraction.
### Future framework work
The following require a reviewed identity, storage, delivery, replay, and observability model before becoming reusable
framework features:
- reusable cross-channel identity linking;
- framework-owned proactive or non-originating delivery;
- fan-out, multicast, selected-channel, active-channel, or all-linked delivery;
- framework-owned delivery observability, dead-letter handling, and replay semantics;
- cross-channel confidentiality and link policy.
These possible framework enhancements are tracked by [ADR-0028](0028-hosting-linking-multicast-enhancements.md). They are
not prerequisites for shipping or using the v1 protocol-helper surface. ADR-0028 was written against the earlier
host/channel framing and must be revised to align with this protocol-helper and execution-state boundary before those
enhancements are implemented.
## Consequences
Positive:
- The host/channel model can be implemented and tested without designing a security-sensitive identity graph.
- Existing and new channel packages can share one Starlette app, middleware stack, lifecycle, and target invocation path.
- Session continuity is explicit and debuggable: two channels share history only when they produce the same `isolation_key`.
- Hook invocation is centralized in the host, so channels do not each invent the call convention.
- The released surface is smaller and easier to inspect: helpers plus state, not a channel framework.
- Protocol helpers can be used from FastAPI, Starlette, Azure Functions, Django, CLI tools, tests, or native SDK webhook
handlers.
- App authors can use the authentication, dependency injection, lifecycle, and background-task tools they already know.
- Session continuity stays explicit and debuggable.
- Workflow checkpointing can still be centralized if needed without making protocol packages own routing.
Negative:
- Apps that need OAuth linking, allowlists, proactive messages, or multicast must continue to implement those behaviors outside the v1 host.
- Some richer cross-channel scenarios from the original design move to a separate decision and validation cycle.
- The host must document `isolation_key` trust clearly because it now provides the shared session boundary.
## Validation Gates
Before this ADR is accepted:
- A sample can expose one target on multiple channels with one `AgentFrameworkHost` and no handwritten Starlette route composition.
- Built-in channel tests prove that routes, commands, startup, and shutdown callbacks are contributed by channels and aggregated by the host.
- Session tests prove that identical `ChannelSession.isolation_key` values resolve to the same cached `AgentSession`, and `reset_session` rotates that mapping.
- Channel tests prove that each channel renders only its own originating response; there is no host-level push, multicast, or active-channel delivery path.
- Workflow tests or samples use an explicit `checkpoint_location`.
- Foundry isolation middleware is documented and covered by integration or contract tests, including the non-Foundry case where raw isolation headers are ignored.
- The v1 API and packages do not expose the removed symbols or packages listed in [Non-goals for v1](#non-goals-for-v1).
- The Python spec is updated to match this simplified contract and uses "public", "stable", or "released" terminology for Agent Framework APIs.
- Multi-protocol samples include explicit route/client code.
- Apps that want a batteries-included ASGI app must write or depend on an app-specific wrapper.
- Existing unreleased code and docs that mention channels, contribution, or hooks must be revised before release.
## More Information
- Follow-up linking and multicast ADR: [ADR-0028](0028-hosting-linking-multicast-enhancements.md)
- Follow-up linking and multicast ADR: [ADR-0028](0028-hosting-linking-multicast-enhancements.md). That ADR still uses
some earlier host/channel terminology and must be aligned before implementation work starts.
## Appendix: Developer experience sketch
The examples below are sketches, not runtime-ready sample code. They show the minimum shape a developer would need to
build: where protocol helpers are called, where app-owned auth/authorization belongs, where state is loaded/stored, and
where native framework code remains in charge.
### Optional execution state
`AgentState` and `WorkflowState` stay small: they are target-specific state holders, not app hosts.
```python
from typing import Protocol
from agent_framework import AgentSession, SupportsAgentRun, Workflow
class SupportsBuild(Protocol):
def build(self) -> Workflow: ...
class SessionStore:
async def get(self, session_id: str) -> AgentSession | None: ...
async def set(self, session_id: str, session: AgentSession) -> None: ...
async def delete(self, session_id: str) -> None: ...
class CheckpointCursorStore:
async def get(self, session_id: str) -> str | None: ...
async def set(self, session_id: str, checkpoint_id: str) -> None: ...
async def delete(self, session_id: str) -> None: ...
class AgentState:
def __init__(self, target: SupportsAgentRun, *, session_store: SessionStore | None = None) -> None: ...
async def get_target(self) -> SupportsAgentRun: ...
async def get_or_create_session(self, session_id: str) -> AgentSession: ...
async def set_session(self, session_id: str, session: AgentSession) -> None: ...
class WorkflowState:
def __init__(self, target: Workflow | SupportsBuild) -> None: ...
async def get_target(self) -> Workflow: ...
```
`WorkflowState` accepts direct `Workflow` instances, workflow factories, and builder-shaped objects with
`build() -> Workflow`. That structurally covers `WorkflowBuilder` and the builders in `agent_framework_orchestrations`
without making `agent-framework-hosting` depend on the orchestration package.
### Responses-only route
This sketch shows the intended Responses-only shape. The protocol package owns the Agent Framework run conversion helpers and
response-id minting details; the application owns FastAPI routing, auth, policy adjustment, and response construction.
```python
import os
from collections.abc import AsyncIterator
from agent_framework import Agent, ResponseStream
from agent_framework.openai import OpenAIChatClient
from agent_framework_hosting import AgentState # pyright: ignore[reportAttributeAccessIssue]
from agent_framework_hosting_responses import create_response_id, responses_from_run, responses_from_streaming_run, responses_session_id, responses_to_run # pyright: ignore[reportAttributeAccessIssue]
from fastapi import Body, FastAPI, Header, HTTPException
from fastapi.responses import JSONResponse, StreamingResponse
app = FastAPI()
agent = Agent(
client=OpenAIChatClient(),
name="Assistant",
instructions="Be concise and helpful.",
)
state = AgentState(agent)
@app.post("/responses")
async def responses(body: dict = Body(...), x_api_key: str | None = Header(default=None)) -> JSONResponse | StreamingResponse:
if x_api_key != os.environ["RESPONSES_API_KEY"]:
raise HTTPException(status_code=401, detail="bad api key")
# parse the request body into a set of AF objects
run = responses_to_run(body)
# get the candidate session id from the body
# can be a resp_* for previous_response_id or a conv_* for a conversation
candidate_session_id = responses_session_id(body)
# create a new response_id for this run
response_id = create_response_id()
# the developer can make any adjustments to the request, i.e.:
run["options"]["store"] = False
run["options"].pop("model", None)
# the options here are of the shape defined by the ChatClient/Agent
# load the session (or create a new one) - this is optional
# verify this caller owns candidate_session_id before loading it; API-key auth
# alone does not prove ownership of a caller-supplied resp_* or conv_* id
session_id = candidate_session_id or response_id
session = await state.get_or_create_session(session_id)
target = await state.get_target()
if run["stream"]:
stream = target.run(
run["messages"],
stream=True,
session=session,
options=run["options"],
)
async def stream_events() -> AsyncIterator[str]:
async for event in responses_from_streaming_run(
stream,
response_id=response_id,
session_id=candidate_session_id,
):
yield event
# agent.run may update the session during stream finalization, so store the post-run session explicitly
await state.set_session(response_id, session)
return StreamingResponse(stream_events(), media_type="text/event-stream")
result = await target.run(
run["messages"],
session=session,
options=run["options"],
)
# agent.run may update the session, so store the post-run session explicitly under the response id
# this might also be skipped, if the app chooses to respect `store=False` policy
await state.set_session(response_id, session)
return JSONResponse(responses_from_run(result, response_id=response_id, session_id=candidate_session_id))
```
### Responses-only Django class-based view
The same helper surface can be used without FastAPI. A Django app owns URL routing, CSRF/auth policy, request parsing,
and `JsonResponse` construction. In a real Django project this would live in the app's normal view module (for example
`assistant/views.py`) and be routed from that app's `urls.py`; Django discovers it through its standard project/app
layout, not through Agent Framework. This sketch shows the non-streaming path only; the streaming branch is the same
state/finalization pattern shown in the FastAPI sketch and is omitted here to avoid duplicating it.
```python
import json
import os
from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient
from agent_framework_hosting import AgentState # pyright: ignore[reportAttributeAccessIssue]
from agent_framework_hosting_responses import create_response_id, responses_from_run, responses_session_id, responses_to_run # pyright: ignore[reportAttributeAccessIssue]
from django.http import HttpRequest, HttpResponseBadRequest, HttpResponseForbidden, JsonResponse
from django.views import View
agent = Agent(
client=OpenAIChatClient(),
name="Assistant",
instructions="Be concise and helpful.",
)
state = AgentState(agent)
class ResponsesView(View):
async def post(self, request: HttpRequest) -> JsonResponse:
if request.headers.get("x-api-key") != os.environ["RESPONSES_API_KEY"]:
return HttpResponseForbidden("bad api key")
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return HttpResponseBadRequest("invalid json")
run = responses_to_run(body)
candidate_session_id = responses_session_id(body)
response_id = create_response_id()
options = run["options"]
# verify this caller owns candidate_session_id before loading it; API-key auth
# alone does not prove ownership of a caller-supplied resp_* or conv_* id
session_id = candidate_session_id or response_id
session = await state.get_or_create_session(session_id)
target = await state.get_target()
result = await target.run(
run["messages"],
session=session,
options=options,
)
await state.set_session(response_id, session)
return JsonResponse(responses_from_run(result, response_id=response_id, session_id=candidate_session_id))
```
@@ -0,0 +1,119 @@
---
status: accepted
contact: rogerbarreto
date: 2026-06-30
deciders: rogerbarreto
consulted: []
informed: []
---
# Per-agent and per-user session-storage isolation for Foundry Hosting
Builds on [ADR-0030](0030-hosted-platform-context-agentserver-2.0.md).
## Context and Problem Statement
A Foundry hosted container can serve many end users (and, in .NET, many agents) over its lifetime. The
`AgentSessionStore` persists each turn's `AgentSession` (which for a workflow agent carries the workflow
checkpoint, and which also carries the tool-approval mapping via `ToolApprovalIdMap` in the session state
bag). [ADR-0030](0030-hosted-platform-context-agentserver-2.0.md) protected cross-user access only through
the strict-resume identity check (a 403 when the persisted `HostedSessionContext.UserId` does not match the
live request). The persisted artifacts themselves were keyed by `conversationId` (+ agent name), not
physically partitioned per user, so a forged `conversation_id` would still resolve to another user's file
path before the identity check rejected it.
The Python hosting package added physical per-user partitioning (`<root>/<user_id>/<context_id>`) plus a
reject-style path-traversal guard. We want .NET to provide the same defense-in-depth, adapted to the .NET
hosting model.
## Decision Drivers
- Defense in depth: a forged/guessed id must not even resolve to another tenant's storage path, independent
of the identity check.
- Multi-agent hosting: a single .NET container hosts multiple agents resolved from keyed DI, so the layout
must isolate per agent as well as per user (Python hosts a single agent and needs no agent layer).
- Path-traversal safety (CWE-22) for the untrusted, platform-injected user id.
- Back-compat for local development (no `x-agent-user-id` header) and for direct/non-hosted store use.
- Keep the change contained and avoid the async-iterator `AsyncLocal` revert hazard from ADR-0030.
## Considered Options
- **Path partition inside `FileSystemAgentSessionStore`**, threading the user id explicitly through the
`AgentSessionStore` API, with self-describing prefixed segments.
- A delegating store that prefixes the conversation id with the user id (the
`IsolationKeyScopedAgentSessionStore` pattern from `Microsoft.Agents.AI.Hosting`). Rejected: still needs the
user id on the read path and yields a flat key rather than nested per-tenant directories.
- An `AsyncLocal<string?>` user-context set by the handler. Rejected: the session is saved in the handler's
`finally` after the streaming `yield`s, where an `AsyncLocal` set up front is reverted (the same hazard
that forced explicit call-id re-application in ADR-0030). Explicit threading is safer and clearer.
- A separate per-user approval store (as in Python). Rejected as unnecessary: see below.
## Decision Outcome
Path layout with self-describing, prefixed segments; user id threaded explicitly:
{root}/ a-{agentName} / u-{userId} / c-{contextId}.json
- `a-` (agent), `u-` (user), `c-` (context) are constant literals applied to the sanitized/validated value,
so a collapsed layout is never ambiguous and a user id can never masquerade as an agent name.
- `contextId` is `HostedConversationKey.Resolve` (conversation_id, else the partition of
previous_response_id, else of the minted response id).
- The agent and context layers are always present (Foundry always deploys a named agent). The only
collapse is the `u-` layer: present when a user id is resolved (Foundry header, or local dev fallback),
absent for raw local runs with no header (`{root}/a-{agent}/c-{conv}.json`). There is no user-only or
no-agent layout.
Other elements:
- `string? userId` was added as a **required** parameter (no default) on `AgentSessionStore.GetSessionAsync` /
`SaveSessionAsync` (a contained, breaking change to the experimental Foundry abstraction; both in-tree
implementations and the two handler call sites were updated). It is required rather than optional so a
caller can never silently persist a session unscoped; a genuine no-user caller (local without the header,
or a non-hosted direct caller) passes `null` explicitly. `AgentFrameworkResponseHandler` resolves the user
id before loading the session.
- Path-traversal guard: the user id is rejected (not sanitized) when it is not a single safe path segment
(path separators, NUL, drive letters, rooted paths, all-dot segments). After building the path, the
fully-resolved path is asserted to remain under the storage root.
- The strict-resume 403 identity check from ADR-0030 is **kept** as the second defense layer (it still
catches a session that reaches the wrong partition, e.g. via a non-partitioning custom store or in-process
tampering).
- **No separate approval store.** The tool-approval mapping lives in `ToolApprovalIdMap` ->
`AgentSessionStateBag`, which is serialized into the session checkpoint, so partitioning the session path
isolates pending approvals per tenant automatically. (Python needs a separate per-user approval store only
because it models approvals as a standalone store.)
## Consequences
Positive:
- Cross-tenant isolation is now defense-in-depth: physical per-agent/per-user partitioning plus the identity
check. Approvals and workflow checkpoints inherit the partitioning because they ride in the session.
- Self-describing prefixes make the on-disk layout auditable and collision-free across collapse cases.
Negative:
- Breaking change to the experimental Foundry `AgentSessionStore` API (added `userId`).
- The on-disk layout and leaf filename change (`<conv>.json` -> `c-<conv>.json`), orphaning sessions written
by the ADR-0030 release. Acceptable for an experimental package; a fresh session is created on next use.
## Out of scope
- Encryption at rest and quota enforcement remain platform concerns.
- Non-Foundry hosting layers can adopt an equivalent scheme independently.
## Update (2026-07-01): local runs no longer fail closed; sample dev provider removed
Superseding the ADR-0026/0030 behavior where a `null` result from `HostedSessionIsolationKeyProvider`
always became a 500, `AgentFrameworkResponseHandler` now branches on `FoundryEnvironment.IsHosted`:
- **Hosted** (`IsHosted == true`, production): a `null` identity is still a hard error (500). Isolation
stays strict; the platform always injects `x-agent-user-id`.
- **Not hosted** (local `docker run` / `dotnet run`): a `null` identity is tolerated. Per-user isolation
is simply not triggered — the handler passes `userId == null` to the store (the documented "no user
partition", `{root}/a-{agent}/c-{conv}.json`), stamps no `HostedSessionContext`, and runs no
strict-resume check. Contributors can run a hosted image locally with zero extra setup.
Consequently the sample-side `DevTemporaryLocalUserIdProvider` and `AddDevTemporaryLocalContributorSetup`
were removed. To simulate distinct users locally, send an `x-agent-user-id` request header; the default
`PlatformHostedSessionIsolationKeyProvider` reads it via `ResponseContext.PlatformContext.UserIdKey`
(the SDK's `PlatformContext.FromRequest` populates it from the header unconditionally, hosted or not).
+304 -276
View File
@@ -1,320 +1,348 @@
---
status: proposed
contact: eavanvalkenburg
date: 2026-06-11
date: 2026-07-08
deciders: eavanvalkenburg
---
# Python hosting core and pluggable channels
# Python protocol helpers and optional execution state
## Scope
This specification is the Python implementation plan for [ADR-0027](../decisions/0027-hosting-channels.md). It documents the simplified v1 host/channel contract only.
This specification is the Python implementation plan for
[ADR-0027](../decisions/0027-hosting-channels.md). It documents the helper-first v1 contract for Python hosting.
The v1 contract is:
- `AgentFrameworkHost` owns one Starlette app, one hostable target, and one or more channels.
- A hostable target is either a `SupportsAgentRun`-compatible agent or a `Workflow`.
- Channels contribute routes, middleware, commands, and lifecycle callbacks.
- Channels parse protocol-native input into `ChannelRequest`.
- Channels render their own originating response.
- Session continuity is explicit: a channel supplies `ChannelSession(isolation_key=...)`, and the host resolves/caches an `AgentSession` for that key.
- The host invokes `ChannelRunHook` and `ChannelResponseHook`; channels provide hook configuration and protocol context.
The host does not link identities, route responses to other channels, run background continuations, or multicast in v1. Those enhancements are tracked in [ADR-0028](../decisions/0028-hosting-linking-multicast-enhancements.md).
- protocol packages expose helper functions that convert protocol-native input to Agent Framework run values;
- protocol packages expose helper functions that convert Agent Framework run results or streams back to protocol-native
payloads or operations;
- application/framework code owns routes, native SDK clients, authentication, command policy, webhooks, response status
codes, and outbound sends;
- `agent-framework-hosting` provides small optional state holders for Agent Framework targets;
- state helpers do not own web apps, route contribution, protocol dispatch, command projection, or native SDK calls.
## Goals
- Let an app expose one agent or workflow on multiple protocols without handwritten Starlette composition.
- Keep protocol parsing and response formatting inside channel packages.
- Provide one session-resolution path shared by all channels.
- Keep the channel authoring surface small enough for new channels to implement.
- Preserve full-fidelity agent and workflow results until a channel decides how to render them.
- Let apps expose agents and workflows from FastAPI, Starlette, Django, Azure Functions, native SDK webhooks, CLIs, and
tests without adopting a host/channel framework.
- Keep protocol parsing and response formatting inside protocol packages.
- Keep session continuity explicit and app-owned at the trust boundary.
- Reuse Agent Framework primitives: `AgentSession`, `CheckpointStorage`, `Agent.run(...)`, `Workflow.run(...)`, and
`ResponseStream`.
- Preserve full-fidelity Agent Framework results until a protocol helper renders them.
## Non-goals for v1
The following are removed from the v1 implementation pass:
### App-owned in v1
- `IdentityLinker`, `IdentityAllowlist`, `AuthPolicy`, and `LinkPolicy`
- `ResponseTarget`, active-channel routing, `all_linked`, fan-out, and multicast
- `ChannelPush` and `ChannelPushCodec`
- `DurableTaskRunner`, `InProcessTaskRunner`, and `RetryPolicy`
- continuation tokens and background delivery
- confidentiality tiers
- `agent-framework-hosting-entra`
- `local_identity_link`
The app builder owns these concerns with normal web-framework, SDK, platform, or application code:
These are follow-up design topics, not hidden requirements of the v1 host.
- authentication, authorization policy, and allowlists;
- deciding whether identities across protocols map to the same `session_id`;
- non-originating sends using native SDK clients;
- background work, durable execution, retry, and replay when app code owns the work;
- routing between multiple agents.
The helper-first model makes app-owned linking and non-originating delivery easier than the old host/channel model because
app code already owns the native SDK clients, authenticated caller context, session id selection, and outbound sends.
### Future framework work
The following require a separate reviewed design before becoming reusable framework features:
- reusable cross-channel identity linking;
- framework-owned proactive or non-originating delivery;
- fan-out, multicast, selected-channel, active-channel, or all-linked delivery;
- framework-owned delivery observability, dead-letter handling, and replay semantics;
- cross-channel confidentiality and link policy.
[ADR-0028](../decisions/0028-hosting-linking-multicast-enhancements.md) tracks possible follow-up work in this area and
must be aligned with the helper-first model before implementation. Old vocabulary such as `IdentityLinker`,
`ResponseTarget`, `ChannelPush`, `ChannelPushCodec`, `DurableTaskRunner`, `RetryPolicy`, and `LinkPolicy` is not v1 API.
## Packages
| Package | Import surface | Contents |
| Package | Import surface | v1 helper-first contents |
|---|---|---|
| `agent-framework-hosting` | `agent_framework_hosting` | `AgentFrameworkHost`, channel protocols, key request/result types, hooks, `reset_session`, state-path helpers. |
| `agent-framework-hosting-responses` | `agent_framework_hosting_responses` | `ResponsesChannel`. |
| `agent-framework-hosting-invocations` | `agent_framework_hosting_invocations` | `InvocationsChannel`. |
| `agent-framework-hosting-telegram` | `agent_framework_hosting_telegram` | `TelegramChannel` and Telegram command helpers. |
| `agent-framework-hosting-activity-protocol` | `agent_framework_hosting_activity_protocol` | `ActivityProtocolChannel` for Activity Protocol over Azure Bot Service. |
| `agent-framework-hosting-discord` | `agent_framework_hosting_discord` | `DiscordChannel` and Discord command/interaction helpers. |
| `agent-framework-foundry-hosting` | `agent_framework.foundry_hosting` | Foundry isolation middleware and Foundry-backed hosting helpers usable with the v1 host. |
| `agent-framework-hosting` | `agent_framework_hosting` | `AgentState`, `WorkflowState`, `SessionStore`, and run-argument `TypedDict`s. |
| `agent-framework-hosting-responses` | `agent_framework_hosting_responses` | Responses helpers: request parsing, session id extraction, response id creation, response rendering, streaming rendering. |
| Future protocol packages | e.g. `agent_framework_hosting_telegram` | Protocol-specific helpers such as `telegram_to_run(...)`, `telegram_from_run(...)`, `telegram_session_id(...)`, and command/media helpers when useful. |
Channel packages may depend on their native SDKs. The core hosting package should not depend on channel SDKs or on top-level legacy protocol hosts.
The core hosting package must not depend on protocol SDKs. Protocol packages may depend on their native protocol SDKs if
needed, but helper functions should stay usable from plain app code and tests.
## Key Types
## Helper naming and families
### `AgentFrameworkHost`
Helper names are protocol-specific. Avoid a generic `protocol_to_run(...)` public surface.
The host constructor accepts:
Protocol packages may provide the following helper families when the protocol has the concept:
- `target`: one `SupportsAgentRun`-compatible object or one `Workflow`
- `channels`: one or more `Channel` instances
- optional Starlette middleware
- optional `state_dir`
- optional workflow `checkpoint_location`
| Helper family | Shape | Purpose |
| --- | --- | --- |
| Run conversion | `<protocol>_to_run(...)` | Convert one protocol-native call/update/request into `Agent.run` or `Workflow.run` values. |
| Final rendering | `<protocol>_from_run(...)` | Convert a final `AgentResponse` or workflow result into protocol-native response payloads or operations. |
| Stream rendering | `<protocol>_from_streaming_run(...)` | Convert `ResponseStream` or workflow updates into protocol-native events or operations. |
| Session id extraction | `<protocol>_session_id(...)` | Extract the protocol's natural continuation/partition key from the call, if present. |
| Command/action parsing | `<protocol>_command(...)` | Parse a protocol-native command/action/operation name without deciding app policy. |
The host exposes:
Examples:
- `app`: the canonical Starlette ASGI application
- `serve(...)`: a convenience wrapper for local serving
- `reset_session(isolation_key: str)`: rotate the cached `AgentSession` for a host-tracked conversation
- `responses_to_run(...)`, `responses_from_run(...)`, `responses_from_streaming_run(...)`,
`responses_session_id(...)`;
- `telegram_to_run(...)`, `telegram_from_run(...)`, `telegram_from_streaming_run(...)`,
`telegram_session_id(...)`, `telegram_command(...)`;
- `activity_to_run(...)`, `activity_from_run(...)`, `activity_session_id(...)`, `activity_command(...)`;
- `discord_to_run(...)`, `discord_from_run(...)`, `discord_session_id(...)`, `discord_command(...)`.
`state_dir` is narrowed to v1 host-owned local files only:
This table is a naming guide, not a required checklist. A protocol package should add only the helpers that match native
protocol concepts and current samples.
- session aliases (`isolation_key` to current `AgentSession` id), and
- workflow checkpoint paths when the app chooses the host-provided file layout.
Protocol-specific helpers may also exist for native details such as `telegram_chat_id(...)`,
`telegram_callback_query_id(...)`, `telegram_media_file_id(...)`, `discord_interaction_id(...)`, `a2a_task_id(...)`,
`a2a_context_id(...)`, or MCP tool/prompt/resource helpers. These helpers should stay side-effect-free. App/native SDK
code performs acknowledgements, sends/edits messages, resolves protected file URLs, applies rate limits, and registers
handlers.
It is not a store for identity links, continuations, active-channel state, delivery attempts, or multicast payloads.
## `agent-framework-hosting` state helpers
Externally supplied isolation keys are trusted only after the channel or host middleware has authenticated and authorized the caller. The host uses `isolation_key` as a partition key; the string itself is not proof of identity or ownership.
### `SessionStore`
### `Channel`
A channel implements a small protocol:
- declare a stable channel id/name,
- contribute routes, middleware, commands, and lifecycle callbacks,
- parse inbound protocol data into `ChannelRequest`,
- call the host through `ChannelContext.run(...)` or `ChannelContext.run_stream(...)`, and
- serialize the returned result to the originating protocol response.
Channels own protocol authentication, signature validation, native command registration, and protocol-specific error bodies.
### `ChannelContribution`
`ChannelContribution` is the channel's host-facing contribution:
- Starlette routes and optional middleware,
- native command descriptors,
- startup and shutdown callbacks, and
- any channel-local metadata needed by the package.
The host aggregates contributions but does not interpret protocol payloads.
### `ChannelRequest`
`ChannelRequest` is the host-neutral request envelope produced by a channel. It carries:
- target input,
- optional `ChannelSession`,
- optional `ChannelIdentity`,
- options and attributes produced by the channel, and
- request metadata useful to hooks and context providers.
The host may pass attributes through to context providers and middleware. Channels should treat attributes as a documented extension bag, not as a cross-channel delivery contract.
### `ChannelSession`
`ChannelSession(isolation_key=...)` is the only v1 session-continuity mechanism.
When a request contains an isolation key:
1. The host looks up or creates the cached `AgentSession` for that key.
2. The target runs with that `AgentSession` when the target is an agent.
3. `reset_session(isolation_key)` rotates the alias so the next request starts a new conversation.
If two channels produce the same isolation key on the same host, they share the same cached session. If they produce different keys, they do not share session state.
### `ChannelIdentity`
`ChannelIdentity` is optional request metadata such as channel id, native user id, tenant id, claims, or display attributes.
In v1, `ChannelIdentity` does not link channels, authorize callers, select delivery destinations, or imply that two identities should share an `AgentSession`. A channel that wants shared history must still produce the same `ChannelSession.isolation_key`.
### Hooks
Hooks are optional and channel-owned:
- `ChannelRunHook`: runs after channel parsing and before host invocation; returns the `ChannelRequest` to execute.
- `ChannelResponseHook`: runs after target completion and before the originating channel renders a one-shot response.
- `ChannelStreamUpdateHook`: the host applies it to streamed updates before the originating channel serializes the stream.
Common uses include adapting chat text into workflow inputs, enforcing deployment-specific options, flattening rich output for text-only protocols, or filtering streamed updates for a protocol. Stream update hooks are update-only; they do not automatically sanitize `get_final_response()` output. Channels choose their response transport from the parsed protocol request before invoking run hooks.
### `HostedRunResult`
`HostedRunResult[T]` wraps the target's full-fidelity result plus the resolved `AgentSession | None`.
- Agent targets produce `HostedRunResult[AgentResponse]`.
- Workflow targets produce `HostedRunResult[WorkflowRunResult]`.
The host does not flatten, filter, or translate the result. Each channel decides how much of the result its protocol can carry.
## Host Behavior
1. `AgentFrameworkHost` builds one Starlette app and asks each channel for its contribution.
2. A channel route receives a protocol-native request.
3. The channel validates/parses the native payload and creates `ChannelRequest`.
4. The channel passes the request, optional `ChannelRunHook`, and protocol-native context to the host.
5. The host invokes `ChannelRunHook`, if configured, and receives the prepared request.
6. The host resolves an `AgentSession` from `ChannelSession.isolation_key` when present.
7. The host invokes the agent or workflow target.
8. The host wraps the result in `HostedRunResult` or the streaming equivalent.
9. The host invokes `ChannelResponseHook`, if configured, for non-streaming/final response shaping.
10. The host applies stream update hooks while the channel consumes streams; the channel renders the originating protocol response.
There is no host-level route from one channel's request to another channel's response in v1.
## Workflow Checkpoints
Workflow checkpointing is explicit. Apps either configure checkpoint storage on the workflow itself or pass a `checkpoint_location` to the host so the workflow dispatch path can use the intended file location.
`state_dir` may provide a conventional location for workflow checkpoint files, but checkpointing is still opt-in and separate from agent session history. Checkpoints are workflow-runtime state, not channel state and not identity-link state.
## Foundry Isolation Middleware
V1 keeps Foundry isolation as middleware rather than as a channel-linking feature.
The middleware is installed only when the Foundry hosting environment flag is present. In that environment it reads Foundry-provided isolation values at the trusted hosting boundary, exposes them as read-only request context for Foundry-aware history or memory providers, and rejects unsafe session resumes when the live isolation context does not match persisted session context. Outside Foundry, raw isolation headers are ignored unless an app supplies its own trusted middleware.
This middleware does not create cross-channel identity links and does not authorize non-Foundry channels.
## Current Channels
### Responses
`ResponsesChannel` exposes the OpenAI-compatible Responses API shape. It maps request body fields such as input, options, and conversation identifiers into `ChannelRequest`, and it renders Responses-compatible one-shot or streaming responses.
Responses session continuity uses a channel-selected `isolation_key`, commonly derived from a response/conversation id, caller-provided session id, Foundry isolation context, or deployment-specific request metadata.
### Invocations
`InvocationsChannel` exposes an invocation endpoint for server-side callers and tools. It maps the request body into `ChannelRequest` and renders the invocation result on the same HTTP response.
Invocations is useful for typed workflow inputs because a `ChannelRunHook` can translate the request body into the workflow's expected input type.
### Telegram
`TelegramChannel` supports webhook or polling transport, native command registration, and message rendering back to the originating Telegram chat.
The channel chooses a default `isolation_key` from Telegram-native data such as chat id, user id, or a configured user/chat scope. A `/new` or equivalent command may call `reset_session` for that isolation key.
### Activity Protocol
`ActivityChannel` supports Activity Protocol requests, typically through Azure Bot Service for Teams, Web Chat, and other Bot Framework-fronted surfaces.
The channel maps incoming `Activity` objects to `ChannelRequest` and renders a reply activity to the originating conversation. Proactive Activity delivery, active-channel routing, and all-linked fan-out are not v1 host semantics.
### Discord
`DiscordChannel` supports Discord messages, slash commands, and interactions as channel-native input.
The channel maps Discord-native user, guild, channel, thread, and interaction data into `ChannelRequest` metadata and a configured `ChannelSession.isolation_key`. It renders the result to the originating Discord response path.
## High-level Samples
### One agent on Responses
`SessionStore` is an in-memory async lookup:
```python
host = AgentFrameworkHost(
target=agent,
channels=[ResponsesChannel()],
class SessionStore:
async def get(self, session_id: str) -> AgentSession | None: ...
async def set(self, session_id: str, session: AgentSession) -> None: ...
async def delete(self, session_id: str) -> None: ...
```
The store does not create sessions. It stores `session_id -> AgentSession` values supplied by callers.
The built-in store has no TTL or eviction. This is intentional for local/dev and simple process-local scenarios: protocols
such as OpenAI Responses can continue from any prior response id. Durable or multi-replica deployments should provide a
durable store and their own TTL/eviction policy.
### `AgentState`
`AgentState` holds an agent target and an optional `SessionStore`:
```python
state = AgentState(agent)
state = AgentState(create_agent)
state = AgentState(create_agent, cache_target=False)
```
The target may be:
- a `SupportsAgentRun` instance;
- a synchronous factory;
- an asynchronous factory;
- an awaitable target.
`AgentState` provides:
- `await get_target()`;
- synchronous `target` only after a target is already available/resolved;
- `session_store`;
- `await get_or_create_session(session_id)`;
- `await set_session(session_id, session)`.
`get_or_create_session(...)` resolves the target and calls `target.create_session(session_id=...)` only when the store has
no session for that id.
Apps must store the post-run session explicitly after `agent.run(...)` or stream finalization:
```python
session = await state.get_or_create_session(session_id)
target = await state.get_target()
result = await target.run(messages, session=session, options=options)
await state.set_session(response_id, session)
```
### `WorkflowState`
`WorkflowState` resolves a workflow target. It does not own checkpoint storage.
The target may be:
- a `Workflow` instance;
- a `WorkflowBuilder` or other object with `build() -> Workflow`;
- a synchronous factory;
- an asynchronous factory;
- an awaitable target.
`WorkflowState` provides:
- `await get_target()`;
- synchronous `target` only after a target is already available/resolved.
Workflow checkpointing uses Agent Framework's existing `CheckpointStorage` abstraction directly. Apps that need
per-session workflow resume should keep an app-owned cursor such as `session_id -> checkpoint_id`. When the app uses
file-backed cursor storage, the file-based checkpoint storage should share the same app storage root and should be
scoped to the current authenticated user/tenant/session bucket, for example
`storage/checkpoints/<session-bucket>/` beside `storage/checkpoint_cursors.json`:
```python
# session_id must already be authenticated and authorized for this caller
target = await workflow_state.get_target()
checkpoint_id = await checkpoint_cursor_store.get(session_id)
if checkpoint_id is None:
result = await target.run(message=workflow_input, checkpoint_storage=checkpoint_storage)
else:
result = await target.run(checkpoint_id=checkpoint_id, checkpoint_storage=checkpoint_storage)
latest = await checkpoint_storage.get_latest(workflow_name=target.name)
if latest is not None:
await checkpoint_cursor_store.set(session_id, latest.checkpoint_id)
```
`Workflow.run(...)` does not currently emit a checkpoint id on `WorkflowRunResult` or normal workflow events by default.
The runner receives checkpoint ids internally from `CheckpointStorage.save(...)`. Apps that own the storage can query
`get_latest(workflow_name=...)` after the run if they need to update a cursor.
## `agent-framework-hosting-responses`
The Responses package provides the helper-first surface for OpenAI Responses-shaped requests.
### Request helpers
- `messages_from_responses_input(input) -> list[Message]`
- `responses_to_run(body) -> AgentRunArgs`
- `responses_session_id(body) -> str | None`
- `create_response_id() -> str`
`responses_to_run(...)` returns values corresponding to `Agent.run(...)`:
```python
run = responses_to_run(body)
messages = run["messages"]
options = run["options"]
stream = run["stream"]
```
It excludes protocol transport/session fields from `options` and remaps known Responses option names such as
`max_output_tokens -> max_tokens`.
`responses_session_id(...)` returns:
- `previous_response_id` when present (`resp_*`);
- otherwise `conversation_id` when present (`conv_*`);
- otherwise `None`.
The helper only extracts the candidate key. App code decides whether to trust and use that key.
### Response helpers
- `responses_from_run(result, *, response_id, session_id=None) -> dict[str, Any]`
- `responses_from_streaming_run(stream, *, response_id, session_id=None) -> AsyncIterator[str]`
`responses_from_run(...)` renders a full Responses JSON payload from an `AgentResponse`. It renders the full set of
OpenAI Responses output item types supported by Agent Framework content.
`responses_from_streaming_run(...)` renders Server-Sent Event strings for a `ResponseStream`. It emits a created event,
text deltas, and a completed event. The final completed payload is produced through `responses_from_run(...)`; the helper
also preserves the model id observed on streaming updates when the finalized `AgentResponse` no longer carries raw model
metadata.
## Security responsibilities
Protocol helper packages parse and render. They do not authenticate callers, authorize access to state, or decide which
side effects are allowed.
Application code that uses these helpers is responsible for:
- authenticating the caller through the app's normal mechanism before using protocol-provided ids;
- authorizing any caller-supplied session, checkpoint, task, context, conversation, thread, or response id before loading
state for it;
- binding externally supplied ids to the authenticated user, tenant, workspace, installation, or chat context before
using them as `SessionStore` keys or checkpoint cursor keys;
- treating `<protocol>_session_id(...)` results as untrusted candidate keys until that ownership check has passed;
- keeping platform-provided isolation helpers fail-closed outside their trusted hosting environment;
- authorizing command/action effects such as reset, cancel, approve, submit, or tool invocation after parsing them;
- opting in explicitly before resolving protected media/resource/file URLs and passing them to a remote model provider;
- persisting post-run session or checkpoint state only after `agent.run(...)`, `workflow.run(...)`, or stream finalization
has updated that state.
## Persistent versus transient hosting
The application builder decides whether the server is persistent or transient.
- Persistent single-process apps, such as a long-running container or web app, may use in-memory state for local
development or simple deployments. Multi-replica persistent apps still need durable state for continuity.
- Transient apps, such as Azure Functions, Foundry Hosted Agents, or any environment where process memory is not a
reliable boundary, must not rely on in-memory `SessionStore` state between calls. They need a durable session store or
a service-owned continuation id.
- Workflow hosts must choose an explicit `CheckpointStorage` and, when they need per-session resume, a durable
`session_id -> checkpoint_id` cursor. File-backed checkpoint storage and file-backed cursor storage should live under
the same app storage root, with checkpoints scoped to the current authenticated user/tenant/session bucket so a
"latest checkpoint" lookup cannot cross conversations. In-process workflow state and in-memory checkpoint cursors do
not survive transient execution.
## Minimal FastAPI Responses shape
This is the shape the local Responses sample should demonstrate. It is not an app framework.
```python
from collections.abc import AsyncIterator
from agent_framework import ResponseStream
from agent_framework_hosting import AgentState
from agent_framework_hosting_responses import (
create_response_id,
responses_from_run,
responses_from_streaming_run,
responses_session_id,
responses_to_run,
)
from fastapi import Body, FastAPI, HTTPException
from fastapi.responses import JSONResponse, StreamingResponse
app = host.app
app = FastAPI()
state = AgentState(create_agent)
@app.post("/responses", response_model=None)
async def responses(body: dict = Body(...)) -> JSONResponse | StreamingResponse:
run = responses_to_run(body)
candidate_session_id = responses_session_id(body)
response_id = create_response_id()
# Verify this caller owns candidate_session_id before loading it.
session_id = candidate_session_id or response_id
session = await state.get_or_create_session(session_id)
target = await state.get_target()
if run["stream"]:
stream = target.run(run["messages"], stream=True, session=session, options=run["options"])
if not isinstance(stream, ResponseStream):
raise HTTPException(status_code=500, detail="agent did not return a response stream")
async def events() -> AsyncIterator[str]:
async for event in responses_from_streaming_run(
stream,
response_id=response_id,
session_id=candidate_session_id,
):
yield event
await state.set_session(response_id, session)
return StreamingResponse(events(), media_type="text/event-stream")
result = await target.run(run["messages"], session=session, options=run["options"])
await state.set_session(response_id, session)
return JSONResponse(responses_from_run(result, response_id=response_id, session_id=candidate_session_id))
```
### One agent on multiple channels
## Validation
```python
host = AgentFrameworkHost(
target=agent,
channels=[
ResponsesChannel(),
InvocationsChannel(),
TelegramChannel(bot_token=os.environ["TELEGRAM_BOT_TOKEN"]),
],
)
Implementation validation must cover:
host.serve(host="localhost", port=8000)
```
The host owns one Starlette app. Each channel contributes its own routes and renders its own response.
### Adapting a request before execution
```python
from dataclasses import replace
def enforce_options(request: ChannelRequest) -> ChannelRequest:
options = dict(request.options or {})
options["temperature"] = 0
return replace(request, options=options)
host = AgentFrameworkHost(
target=agent,
channels=[ResponsesChannel(run_hook=enforce_options)],
)
```
### Workflow with explicit checkpoints
```python
host = AgentFrameworkHost(
target=workflow,
channels=[InvocationsChannel(run_hook=adapt_to_workflow_input)],
checkpoint_location=Path("./.af-hosting/workflow_checkpoints"),
)
```
The hook adapts channel-native input to the workflow's typed input. Checkpoints use the explicit workflow checkpoint location, not identity-link or delivery storage.
### Message channel reset command
```python
async def new_chat(context):
if context.request.session is not None:
await context.host.reset_session(context.request.session.isolation_key)
await context.reply("Started a new conversation.")
```
Telegram, Activity Protocol, and Discord can expose equivalent native commands when their protocols support them.
## Follow-up Enhancements
See [ADR-0028](../decisions/0028-hosting-linking-multicast-enhancements.md) for the deferred design covering:
- cross-channel identity linking,
- authorization and allowlists,
- non-originating response delivery,
- active-channel routing,
- multicast and all-linked delivery,
- background runs and continuation tokens,
- durable delivery runners,
- retry/replay semantics, and
- payload serialization.
Those enhancements must layer on top of this v1 contract without requiring v1 users to adopt them.
## Validation Gates
The Python implementation should be considered complete when:
- a sample uses one `AgentFrameworkHost` with multiple channels and no manual Starlette route composition,
- each current channel has contract tests for route contribution, lifecycle, request parsing, hooks, and originating response rendering,
- session tests prove shared `isolation_key` values share an `AgentSession` and `reset_session` rotates it,
- workflow tests or samples use explicit `checkpoint_location`,
- Foundry isolation middleware is covered by integration or contract tests,
- no v1 package exposes the removed linking, multicast, durable-runner, or continuation APIs, and
- this spec and ADR-0027 remain aligned.
- `SessionStore` plain get/set/delete behavior;
- `AgentState` target resolution, target caching, and get-or-create session behavior;
- `WorkflowState` target resolution for direct workflows, factories, `WorkflowBuilder`, and orchestration-style builders;
- Responses request parsing and option remapping;
- Responses session id extraction;
- Responses response rendering, including rich output item mapping;
- Responses streaming SSE rendering;
- HTTP round-trip tests showing a native FastAPI route using `AgentState` and Responses helpers;
- sample type checking for the local Responses sample.
+20 -13
View File
@@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="12.20.0" />
<PackageVersion Include="Anthropic" Version="12.31.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.6.0" />
<PackageVersion Include="Aspire.Hosting" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
@@ -27,10 +27,10 @@
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.5" />
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.6" />
<PackageVersion Include="Azure.Search.Documents" Version="12.0.0" />
<PackageVersion Include="Azure.AI.Projects" Version="2.1.0-beta.3" />
<PackageVersion Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.9.0-beta.1" />
<PackageVersion Include="Azure.Core" Version="1.57.0" />
<PackageVersion Include="Azure.Core" Version="1.60.0" />
<PackageVersion Include="Azure.Identity" Version="1.21.0" />
<PackageVersion Include="DotNetEnv" Version="3.1.1" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.5.0" />
@@ -42,18 +42,24 @@
<!-- Newtonsoft.Json -->
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<!-- System.* -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.8" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.9" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.5" />
<PackageVersion Include="System.ClientModel" Version="1.13.0" />
<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.8" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.9" />
<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.5" />
<PackageVersion Include="System.Text.Json" Version="10.0.8" />
<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.8" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
@@ -70,7 +76,8 @@
<!-- Microsoft.AspNetCore.* -->
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
<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.6.0" />
@@ -87,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.8" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
<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.8" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<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="9.7.0" />
@@ -100,7 +107,7 @@
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.67.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.67.0-preview" />
<!-- Agent SDKs -->
<PackageVersion Include="GitHub.Copilot.SDK" Version="1.0.0" />
<PackageVersion Include="GitHub.Copilot.SDK" Version="1.0.5" />
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.171-beta" />
<!-- M365 Agents SDK -->
<PackageVersion Include="AdaptiveCards" Version="3.1.0" />
@@ -120,7 +127,7 @@
<PackageVersion Include="OllamaSharp" Version="5.4.8" />
<PackageVersion Include="OpenAI" Version="2.10.0" />
<!-- Identity -->
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.83.1" />
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.84.2" />
<!-- Workflows -->
<PackageVersion Include="Microsoft.Agents.ObjectModel" Version="2026.2.4.1" />
<PackageVersion Include="Microsoft.Agents.ObjectModel.Json" Version="2026.2.4.1" />
+1 -2
View File
@@ -123,6 +123,7 @@
<File Path="samples/02-agents/Harness/README.md" />
<Project Path="samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step01_MeetYourClaw/Claw_Step01_MeetYourClaw.csproj" />
<Project Path="samples/02-agents/Harness/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/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,7 +606,6 @@
<Project Path="src/Aspire.Hosting.AgentFramework.DevUI/Aspire.Hosting.AgentFramework.DevUI.csproj" />
<Project Path="src/Microsoft.Agents.AI.A2A/Microsoft.Agents.AI.A2A.csproj" />
<Project Path="src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj" />
<Project Path="src/Microsoft.Agents.AI.AGUI/Microsoft.Agents.AI.AGUI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj" />
<Project Path="src/Microsoft.Agents.AI.AzureAI.Persistent/Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
<Project Path="src/Microsoft.Agents.AI.CopilotStudio/Microsoft.Agents.AI.CopilotStudio.csproj" />
@@ -664,7 +664,6 @@
<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.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.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,7 +4,6 @@
"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.AGUI\\Microsoft.Agents.AI.AGUI.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",
+5 -5
View File
@@ -335,8 +335,8 @@ internal static class AgentsSamples
{
Name = "Agent_Step01_FileBasedSkills",
ProjectPath = "samples/02-agents/AgentSkills/Agent_Step01_FileBasedSkills",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
MustContain =
[
"Converting units with file-based skills",
@@ -354,8 +354,8 @@ internal static class AgentsSamples
{
Name = "Agent_Step06_McpBasedSkills",
ProjectPath = "samples/02-agents/AgentSkills/Agent_Step06_McpBasedSkills",
RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"],
OptionalEnvironmentVariables = ["AZURE_OPENAI_DEPLOYMENT_NAME"],
RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT"],
OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
MustContain =
[
"Discovering MCP-based skills",
@@ -701,7 +701,7 @@ internal static class AgentsSamples
OptionalEnvironmentVariables = ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
ExpectedOutputDescription =
[
"The output should contain a list of countries or information about countries that use the EUR currency.",
"The output should contain the current EUR exchange rate against USD and GBP as numeric values.",
"The output should not contain error messages or stack traces.",
],
},
+3 -3
View File
@@ -1,14 +1,14 @@
<Project>
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.12.0</VersionPrefix>
<VersionPrefix>1.13.0</VersionPrefix>
<RCNumber>1</RCNumber>
<DateSuffix>260629</DateSuffix>
<DateSuffix>260703</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.12.0</GitTag>
<GitTag>1.13.0</GitTag>
<Configurations>Debug;Release;Publish</Configurations>
<IsPackable>true</IsPackable>
+4 -4
View File
@@ -35,7 +35,7 @@ A basic AG-UI server and client that demonstrate the foundational concepts.
A basic AG-UI server that hosts an AI agent accessible via HTTP. Demonstrates:
- Creating an ASP.NET Core web application
- Setting up an AG-UI server endpoint with `MapAGUI`
- Setting up an AG-UI server endpoint with `MapAGUIServer`
- Creating an AI agent from an Azure OpenAI chat client
- Streaming responses via Server-Sent Events (SSE)
@@ -204,7 +204,7 @@ dotnet run
### Server-Side
1. Client sends HTTP POST request with messages
2. ASP.NET Core endpoint receives the request via `MapAGUI`
2. ASP.NET Core endpoint receives the request via `MapAGUIServer`
3. Agent processes messages using Agent Framework
4. Responses are streamed back as Server-Sent Events (SSE)
@@ -214,14 +214,14 @@ dotnet run
2. Server responds with SSE stream
3. Client parses events into `AgentResponseUpdate` objects
4. Updates are displayed based on content type
5. `ConversationId` maintains conversation context
5. The client sends the full message history each turn (the stateless AG-UI client does not rely on a server-assigned `ConversationId`)
### Protocol Features
- **HTTP POST** for requests
- **Server-Sent Events (SSE)** for streaming responses
- **JSON** for event serialization
- **Thread IDs** (as `ConversationId`) for conversation context
- **Thread IDs** (read from the `RUN_STARTED` event's raw representation) for conversation context. `AGUIChatClient` is stateless and intentionally does not surface a `ConversationId`.
- **Run IDs** (as `ResponseId`) for tracking individual executions
## Security considerations
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,7 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
<PackageReference Include="AGUI.Client" />
</ItemGroup>
</Project>
@@ -1,7 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
using AGUI.Abstractions;
using AGUI.Client;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AGUI;
using Microsoft.Extensions.AI;
string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888";
@@ -14,7 +15,7 @@ using HttpClient httpClient = new()
Timeout = TimeSpan.FromSeconds(60)
};
AGUIChatClient chatClient = new(httpClient, serverUrl);
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
AIAgent agent = chatClient.AsAIAgent(
name: "agui-client",
@@ -49,7 +50,7 @@ try
// Stream the response
bool isFirstUpdate = true;
string? sessionId = null;
string? threadId = null;
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
{
@@ -58,9 +59,11 @@ try
// First update indicates run started
if (isFirstUpdate)
{
sessionId = chatUpdate.ConversationId;
// 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 - Session: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]");
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
Console.ResetColor();
isFirstUpdate = false;
}
@@ -84,7 +87,7 @@ try
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"\n[Run Finished - Session: {sessionId}]");
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
Console.ResetColor();
}
}
@@ -8,7 +8,7 @@ using OpenAI.Chat;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
builder.Services.AddAGUI();
builder.Services.AddAGUIServer();
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
@@ -36,6 +36,6 @@ AIAgent agent = chatClient.AsAIAgent(
instructions: "You are a helpful assistant.");
// Map the AG-UI agent endpoint
app.MapAGUI("/", agent);
app.MapAGUIServer("/", agent);
await app.RunAsync();
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,7 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
<PackageReference Include="AGUI.Client" />
</ItemGroup>
</Project>
@@ -1,7 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
using AGUI.Abstractions;
using AGUI.Client;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AGUI;
using Microsoft.Extensions.AI;
string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888";
@@ -14,7 +15,7 @@ using HttpClient httpClient = new()
Timeout = TimeSpan.FromSeconds(60)
};
AGUIChatClient chatClient = new(httpClient, serverUrl);
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
AIAgent agent = chatClient.AsAIAgent(
name: "agui-client",
@@ -49,7 +50,7 @@ try
// Stream the response
bool isFirstUpdate = true;
string? sessionId = null;
string? threadId = null;
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
{
@@ -58,9 +59,11 @@ try
// First update indicates run started
if (isFirstUpdate)
{
sessionId = chatUpdate.ConversationId;
// 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 - Session: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]");
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
Console.ResetColor();
isFirstUpdate = false;
}
@@ -116,7 +119,7 @@ try
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"\n[Run Finished - Session: {sessionId}]");
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
Console.ResetColor();
}
}
@@ -14,7 +14,7 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.TypeInfoResolverChain.Add(SampleJsonSerializerContext.Default));
builder.Services.AddAGUI();
builder.Services.AddAGUIServer();
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
@@ -93,7 +93,7 @@ ChatClientAgent agent = chatClient.AsAIAgent(
tools: tools);
// Map the AG-UI agent endpoint
app.MapAGUI("/", agent);
app.MapAGUIServer("/", agent);
await app.RunAsync();
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,7 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
<PackageReference Include="AGUI.Client" />
</ItemGroup>
</Project>
@@ -1,8 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using AGUI.Abstractions;
using AGUI.Client;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AGUI;
using Microsoft.Extensions.AI;
string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:8888";
@@ -26,7 +27,7 @@ using HttpClient httpClient = new()
Timeout = TimeSpan.FromSeconds(60)
};
AGUIChatClient chatClient = new(httpClient, serverUrl);
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
AIAgent agent = chatClient.AsAIAgent(
name: "agui-client",
@@ -62,7 +63,7 @@ try
// Stream the response
bool isFirstUpdate = true;
string? sessionId = null;
string? threadId = null;
await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, session))
{
@@ -71,9 +72,11 @@ try
// First update indicates run started
if (isFirstUpdate)
{
sessionId = chatUpdate.ConversationId;
// 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 - Session: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]");
Console.WriteLine($"\n[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
Console.ResetColor();
isFirstUpdate = false;
}
@@ -109,7 +112,7 @@ try
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"\n[Run Finished - Session: {sessionId}]");
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
Console.ResetColor();
}
}
@@ -8,7 +8,7 @@ using OpenAI.Chat;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
builder.Services.AddAGUI();
builder.Services.AddAGUIServer();
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
@@ -36,6 +36,6 @@ AIAgent agent = chatClient.AsAIAgent(
instructions: "You are a helpful assistant.");
// Map the AG-UI agent endpoint
app.MapAGUI("/", agent);
app.MapAGUIServer("/", agent);
await app.RunAsync();
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,7 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
<PackageReference Include="AGUI.Client" />
</ItemGroup>
</Project>
@@ -1,8 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using AGUI.Client;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AGUI;
using Microsoft.Extensions.AI;
string serverUrl = Environment.GetEnvironmentVariable("AGUI_SERVER_URL") ?? "http://localhost:5100";
@@ -13,7 +13,7 @@ using HttpClient httpClient = new()
Timeout = TimeSpan.FromSeconds(60)
};
AGUIChatClient chatClient = new(httpClient, serverUrl);
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
// Create agent
ChatClientAgent baseAgent = chatClient.AsAIAgent(
@@ -25,7 +25,7 @@ builder.Services.AddHttpLogging(logging =>
builder.Services.AddHttpClient().AddLogging();
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.TypeInfoResolverChain.Add(ApprovalJsonContext.Default));
builder.Services.AddAGUI();
builder.Services.AddAGUIServer();
// WARNING: When adding session persistence (e.g., WithInMemorySessionStore), or running in production,
// make sure to also register a SessionIsolationKeyProvider to scope sessions by principal in multi-user
@@ -73,5 +73,5 @@ ChatClientAgent baseAgent = openAIChatClient.AsAIAgent(
// Wrap with ServerFunctionApprovalAgent
var agent = new ServerFunctionApprovalAgent(baseAgent, jsonOptions.SerializerOptions);
app.MapAGUI("/", agent);
app.MapAGUIServer("/", agent);
await app.RunAsync();
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,7 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.AGUI\Microsoft.Agents.AI.AGUI.csproj" />
<PackageReference Include="AGUI.Client" />
</ItemGroup>
</Project>
@@ -2,8 +2,9 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using AGUI.Abstractions;
using AGUI.Client;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AGUI;
using Microsoft.Extensions.AI;
using RecipeClient;
@@ -17,7 +18,7 @@ using HttpClient httpClient = new()
Timeout = TimeSpan.FromSeconds(60)
};
AGUIChatClient chatClient = new(httpClient, serverUrl);
AGUIChatClient chatClient = new(new(httpClient, serverUrl));
AIAgent baseAgent = chatClient.AsAIAgent(
name: "recipe-client",
@@ -65,7 +66,7 @@ try
// Stream the response
bool isFirstUpdate = true;
string? sessionId = null;
string? threadId = null;
bool stateReceived = false;
Console.WriteLine();
@@ -77,9 +78,11 @@ try
// First update indicates run started
if (isFirstUpdate)
{
sessionId = chatUpdate.ConversationId;
// 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 - Session: {chatUpdate.ConversationId}, Run: {chatUpdate.ResponseId}]");
Console.WriteLine($"[Run Started - Thread: {threadId}, Run: {chatUpdate.ResponseId}]");
Console.ResetColor();
isFirstUpdate = false;
}
@@ -113,7 +116,7 @@ try
}
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"\n[Run Finished - Session: {sessionId}]");
Console.WriteLine($"\n[Run Finished - Thread: {threadId}]");
Console.ResetColor();
// Display final state if received
@@ -12,7 +12,7 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.TypeInfoResolverChain.Add(RecipeSerializerContext.Default));
builder.Services.AddAGUI();
builder.Services.AddAGUIServer();
// Configure to listen on port 8888
builder.WebHost.UseUrls("http://localhost:8888");
@@ -61,6 +61,6 @@ AIAgent baseAgent = chatClient.AsAIAgent(
AIAgent agent = new SharedStateAgent(baseAgent, jsonOptions.SerializerOptions);
// Map the AG-UI agent endpoint
app.MapAGUI("/", agent);
app.MapAGUIServer("/", agent);
await app.RunAsync();
@@ -2,6 +2,8 @@
using System.Runtime.CompilerServices;
using System.Text.Json;
using AGUI.Abstractions;
using AGUI.Server;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
@@ -34,10 +36,9 @@ internal sealed class SharedStateAgent : DelegatingAIAgent
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Check if the client sent state in the request
if (options is not ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } chatRunOptions ||
!properties.TryGetValue("ag_ui_state", out object? stateObj) ||
stateObj is not JsonElement state ||
state.ValueKind != JsonValueKind.Object)
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))
@@ -12,7 +12,7 @@ using Microsoft.Extensions.AI;
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
const string AgentInstructions = "You are a helpful assistant that can use the countries API to retrieve information about countries by their currency code. When calling the API, always pass fields=name to limit the response to just country names.";
const string AgentInstructions = "You are a helpful assistant that can retrieve the latest currency exchange rates using the Frankfurter API. Always call the API to get live data rather than guessing.";
// 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.
@@ -25,44 +25,45 @@ AIAgent agent = aiProjectClient.AsAIAgent(deploymentName,
name: "OpenAPIToolsAgent",
tools: [openApiTool]);
// Run the agent with a question about countries
Console.WriteLine(await agent.RunAsync("What countries use the Euro (EUR) as their currency? Please list them."));
// Run the agent with a question about EUR exchange rates
Console.WriteLine(await agent.RunAsync("What is the latest EUR exchange rate against the US Dollar (USD) and British Pound (GBP)?"));
OpenApiFunctionDefinition CreateOpenAPIFunctionDefinition()
{
// A simple OpenAPI specification for the REST Countries API
const string CountriesOpenApiSpec = """
// OpenAPI spec for Frankfurter — a free, no-auth exchange rate API backed by ECB data.
// See https://www.frankfurter.dev/ for documentation.
const string FrankfurterOpenApiSpec = """
{
"openapi": "3.1.0",
"info": {
"title": "REST Countries API",
"description": "Retrieve information about countries by currency code",
"version": "v3.1"
"title": "Frankfurter Exchange Rate API",
"description": "Free currency exchange rates from the European Central Bank",
"version": "v1"
},
"servers": [
{
"url": "https://restcountries.com/v3.1"
"url": "https://api.frankfurter.dev/v1"
}
],
"paths": {
"/currency/{currency}": {
"/latest": {
"get": {
"description": "Get countries that use a specific currency code (e.g., USD, EUR, GBP)",
"operationId": "GetCountriesByCurrency",
"description": "Get the latest exchange rates for a given base currency",
"operationId": "GetLatestExchangeRates",
"parameters": [
{
"name": "currency",
"in": "path",
"description": "Currency code (e.g., USD, EUR, GBP)",
"required": true,
"name": "from",
"in": "query",
"description": "Base currency code (e.g. EUR, USD, GBP). Defaults to EUR.",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "fields",
"name": "to",
"in": "query",
"description": "Comma-separated list of fields to include in the response (e.g., name,currencies)",
"description": "Comma-separated list of target currency codes (e.g. USD,GBP,JPY).",
"required": false,
"schema": {
"type": "string"
@@ -71,20 +72,14 @@ OpenApiFunctionDefinition CreateOpenAPIFunctionDefinition()
],
"responses": {
"200": {
"description": "Successful response with list of countries",
"description": "Latest exchange rates",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object"
}
"type": "object"
}
}
}
},
"404": {
"description": "No countries found for the currency"
}
}
}
@@ -93,12 +88,11 @@ OpenApiFunctionDefinition CreateOpenAPIFunctionDefinition()
}
""";
// Create the OpenAPI function definition
return new(
"get_countries",
BinaryData.FromString(CountriesOpenApiSpec),
"get_exchange_rates",
BinaryData.FromString(FrankfurterOpenApiSpec),
new OpenAPIAnonymousAuthenticationDetails())
{
Description = "Retrieve information about countries by currency code"
Description = "Get live currency exchange rates from the European Central Bank via Frankfurter"
};
}
@@ -5,7 +5,7 @@ This sample shows how to use OpenAPI tools with a `ChatClientAgent` using the Re
## What this sample demonstrates
- Defining an OpenAPI specification inline
- Creating an `OpenAPIFunctionDefinition` for the REST Countries API
- Creating an `OpenAPIFunctionDefinition` for the Frankfurter exchange rate API
- Using `FoundryAITool.CreateOpenApiTool()` with `ChatClientAgent`
- Server-side execution of OpenAPI tool calls
@@ -90,7 +90,7 @@ static async Task<string> CreateSampleToolboxAsync(string name, string endpoint,
// Delete existing toolbox if present (ignore 404).
try
{
await toolboxClient.DeleteToolboxAsync(name);
await toolboxClient.DeleteAsync(name);
Console.WriteLine($"Deleted existing toolbox '{name}'");
}
catch (ClientResultException ex) when (ex.Status == 404)
@@ -99,12 +99,13 @@ static async Task<string> CreateSampleToolboxAsync(string name, string endpoint,
}
// Create a fresh version with a single MCP tool.
ProjectsAgentTool mcpTool = ProjectsAgentTool.AsProjectTool(ResponseTool.CreateMcpTool(
serverLabel: "api-specs",
serverUri: new Uri("https://gitmcp.io/Azure/azure-rest-api-specs"),
toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)));
MCPToolboxTool mcpTool = new("api-specs")
{
ServerUri = new Uri("https://gitmcp.io/Azure/azure-rest-api-specs"),
ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval),
};
ToolboxVersion created = (await toolboxClient.CreateToolboxVersionAsync(
ToolboxVersion created = (await toolboxClient.CreateVersionAsync(
name: name,
tools: [mcpTool],
description: "Sample toolbox with an MCP tool — created by Agent_Step25 sample.")).Value;
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);MAAI001</NoWarn>
<NoWarn>$(NoWarn);</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -38,7 +38,17 @@ AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredentia
Instructions = "You are a helpful assistant that can convert units.",
},
AIContextProviders = [skillsProvider],
});
})
.AsBuilder()
.UseToolApproval(new ToolApprovalAgentOptions
{
// NOTE: Auto-approving all skill tools is done here for simplicity in
// this demonstration. In production, you should prompt the user before
// allowing script execution. See Agent_Step07_SkillsAutoApproval for a
// walkthrough of the full approval flow.
AutoApprovalRules = [AgentSkillsProvider.AllToolsAutoApprovalRule],
})
.Build();
// --- Example: Unit conversion ---
Console.WriteLine("Converting units with file-based skills");
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);MAAI001</NoWarn>
<NoWarn>$(NoWarn);</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);MAAI001;IDE0051</NoWarn>
<NoWarn>$(NoWarn);IDE0051</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);MAAI001;IDE0051</NoWarn>
<NoWarn>$(NoWarn);IDE0051</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);MAAI001;CA1812;IDE0051</NoWarn>
<NoWarn>$(NoWarn);CA1812;IDE0051</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);MAAI001;MCPEXP001</NoWarn>
<NoWarn>$(NoWarn);MCPEXP001</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -63,7 +63,17 @@ AIAgent agent = new AIProjectClient(new Uri(openAiEndpoint), new DefaultAzureCre
Instructions = "You are a helpful assistant. Use available skills to answer the user.",
},
AIContextProviders = [skillsProvider],
});
})
.AsBuilder()
.UseToolApproval(new ToolApprovalAgentOptions
{
// NOTE: Auto-approving all skill tools is done here for simplicity in
// this demonstration. In production, you should prompt the user before
// allowing skill tools to execute. See Agent_Step07_SkillsAutoApproval
// for a walkthrough of the full approval flow.
AutoApprovalRules = [AgentSkillsProvider.AllToolsAutoApprovalRule],
})
.Build();
// --- Run ---
Console.WriteLine(new string('-', 60));
@@ -32,3 +32,12 @@ $env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-5.4-mini"
```powershell
dotnet run
```
## Security Considerations
Discovering skills over MCP means an external MCP server controls what skill content (including
instructions and, for archive-type entries, extracted files) reaches the agent. A compromised or
untrustworthy server could return adversarial content designed to manipulate the agent (indirect
prompt injection) or to exfiltrate data through skill instructions/scripts. Only connect `UseMcpSkills`
to MCP servers you have vetted and trust, and keep the conservative archive size/file-count limits in
`AgentMcpSkillsSourceOptions` unless you have a specific reason to raise them.
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -130,3 +130,13 @@ AIAgent agent = agentChatClient
This places the compaction provider at the agent level instead of the chat client level, which allows you to use different compaction strategies for different agents that share the same chat client.
> Note: In this mode the `CompactionProvider` is not engaged during the tool calling loop. Agent-level `AIContextProviders` run before chat history is stored, so any synthetic summary messages produced by `CompactionProvider` can become part of the persisted history when using `ChatHistoryProvider`. If you want to compact only the request context while preserving the original stored history, register `CompactionProvider` on the `ChatClientBuilder` via `UseAIContextProviders(...)` instead of on `ChatClientAgentOptions`.
## Security Considerations
Most compaction strategies in this pipeline (tool-result summarization, sliding window, truncation) only
remove or reorder existing messages and carry no additional risk. `SummarizationCompactionStrategy` is
the exception: it calls out to an LLM to produce replacement summary content that permanently becomes
part of chat history. A compromised or malicious summarization service could return a summary containing
unsafe instructions, creating a persistent indirect-prompt-injection vector. Using
`SummarizationCompactionStrategy` is optional and requires explicit configuration — only point its
`IChatClient` at a summarization service you trust as much as the primary model.
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="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.Tools.Shell\Microsoft.Agents.AI.Tools.Shell.csproj" />
<ProjectReference Include="..\..\Harness_Shared_Console\Harness_Shared_Console.csproj" />
<ProjectReference Include="..\..\Harness_Shared_Console_OpenAI\Harness_Shared_Console_OpenAI.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="skills\**\*" CopyToOutputDirectory="PreserveNewest" />
<Content Include="working\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net.Http.Headers;
using Azure.Core;
using ModelContextProtocol.Client;
namespace ClawSample;
/// <summary>
/// Helpers for wiring centrally-managed <b>Foundry skills</b> into the claw via a Foundry Toolbox
/// MCP endpoint. These are opt-in: skills published to the toolbox are discovered at runtime, so
/// they can be managed and updated without changing or redeploying the agent.
/// </summary>
internal static class FoundrySkills
{
/// <summary>
/// Connects to a Foundry Toolbox MCP endpoint and returns a connected <see cref="McpClient"/>.
/// The caller owns the returned client and its HTTP client.
/// </summary>
/// <param name="toolboxMcpServerUrl">The Foundry Toolbox MCP server URL.</param>
/// <param name="credential">Credential used to obtain a bearer token for the toolbox.</param>
/// <returns>The connected MCP client and the underlying HTTP client; both must be disposed by the caller.</returns>
public static async Task<(McpClient McpClient, HttpClient HttpClient)> ConnectAsync(
string toolboxMcpServerUrl,
TokenCredential credential)
{
var httpClient = new HttpClient(new BearerTokenHandler(credential, "https://ai.azure.com/.default")
{
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));
return (mcpClient, httpClient);
}
catch
{
// The MCP client never took ownership of the HTTP client, so dispose it here.
httpClient.Dispose();
throw;
}
}
private sealed class BearerTokenHandler(TokenCredential credential, string scope) : 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);
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,228 @@
// Copyright (c) Microsoft. All rights reserved.
// "Scaling its capabilities" — Post 3 of the "Build your own claw and agent harness with Microsoft
// Agent Framework" series.
// See: https://devblogs.microsoft.com/agent-framework/agent-harness-scaling-the-claw-or-harness-capabilities/.
//
// This sample builds on Post 2's personal finance assistant and makes it *more capable* in four ways:
// 1. Skills — package finance know-how (valuation, risk-scoring) as discoverable SKILL.md
// files the agent loads on demand. Optionally fold in centrally-managed Foundry
// skills from a Foundry Toolbox MCP endpoint (opt-in via FOUNDRY_TOOLBOX_MCP_SERVER_URL).
// 2. Shell — a sandboxed shell, confined to the trade-confirmation vault, that the agent
// uses to reorganize the accumulated confirmation files (year/month, rename,
// archive). Guarded by a deny-list policy and a confined working directory.
// 3. CodeAct — the agent writes and runs Python to crunch portfolio numbers, in a sandboxed
// Hyperlight micro-VM (needs hardware virtualization).
// 4. Background agents — fan out a per-ticker research sub-agent so several tickers are researched
// concurrently, then aggregated.
//
// Special commands (handled by the shared HarnessConsole):
// /todos — Display the current todo list without invoking the agent.
// /mode — Get or set the current agent mode.
// /exit — End the session.
#pragma warning disable OPENAI001 // Suppress experimental API warnings for Responses API usage.
#pragma warning disable MAAI001 // Suppress experimental API warnings for Agents AI experiments.
using System.ClientModel.Primitives;
using Azure.AI.Projects;
using Azure.Identity;
using ClawSample;
using Harness.Shared.Console;
using Harness.Shared.Console.OpenAI;
using Harness.Shared.Console.ToolFormatters;
using HyperlightSandbox.Guest.Python;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hyperlight;
using Microsoft.Agents.AI.Tools.Shell;
using Microsoft.Extensions.AI;
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4";
// The two folders the claw works in: the working folder (portfolio.csv, reports) and the
// trade-confirmation "vault" inside it that the shell will reorganize.
var workingDir = Path.Combine(AppContext.BaseDirectory, "working");
var vaultDir = Path.Combine(workingDir, "confirmations");
var skillsDir = Path.Combine(AppContext.BaseDirectory, "skills");
// <instructions>
var 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.
""";
// </instructions>
// <create_client>
// Construct an IChatClient backed by a Microsoft Foundry project (see Post 1 for details).
var credential = new DefaultAzureCredential();
var projectClient = new AIProjectClient(
new Uri(endpoint),
// 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.
credential,
new AIProjectClientOptions { RetryPolicy = new ClientRetryPolicy(3) });
IChatClient chatClient = projectClient
.GetProjectOpenAIClient()
.GetResponsesClient()
.AsIChatClient(deploymentName);
// </create_client>
// <skills>
// The harness turns a skills provider on by default (it discovers SKILL.md files from the working
// directory). Here we build our own so we can point it at this sample's skills/ folder and, when
// configured, fold in centrally-managed Foundry skills — all behind one provider.
var skillsBuilder = new AgentSkillsProviderBuilder()
// File-based skills: valuation and risk-scoring. SubprocessScriptRunner runs their Python scripts.
.UseFileSkills([skillsDir], scriptRunner: new SubprocessScriptRunner().RunAsync);
// Foundry skills (opt-in): discovered live from a Foundry Toolbox MCP endpoint, so they can be
// managed and updated centrally without changing or redeploying this agent.
HttpClient? toolboxHttpClient = null;
ModelContextProtocol.Client.McpClient? toolboxMcpClient = null;
var toolboxUrl = Environment.GetEnvironmentVariable("FOUNDRY_TOOLBOX_MCP_SERVER_URL");
if (!string.IsNullOrWhiteSpace(toolboxUrl))
{
(toolboxMcpClient, toolboxHttpClient) = await FoundrySkills.ConnectAsync(toolboxUrl, credential);
skillsBuilder.UseMcpSkills(toolboxMcpClient);
Console.WriteLine("Foundry skills enabled (Toolbox MCP).");
}
else
{
Console.WriteLine("Foundry skills disabled. Set FOUNDRY_TOOLBOX_MCP_SERVER_URL to enable them.");
}
AgentSkillsProvider skillsProvider = skillsBuilder.Build();
// </skills>
// <background>
// Background agents: a lean, web-search-only research sub-agent. Passing it to the harness exposes
// the background_agents_* tools so the claw can start several research tasks concurrently and
// collect the results.
AIAgent researchAgent = ResearchAgent.Create(chatClient);
// </background>
// <shell>
// A sandboxed shell, confined to the trade-confirmation vault. ConfineWorkingDirectory re-anchors
// every command to the vault, and the deny-list policy pre-filters obviously destructive commands.
// (Patterns are a UX guardrail, not a security boundary — for hard isolation use DockerShellExecutor.)
await using var shell = new LocalShellExecutor(new LocalShellExecutorOptions
{
WorkingDirectory = vaultDir,
ConfineWorkingDirectory = true,
Policy = new ShellPolicy(denyList:
[
@"\brm\s+-rf\b",
@"\bsudo\b",
@":\(\)\s*\{", // fork-bomb shape
@"\bmkfs\b",
@">\s*/dev/sd",
]),
Timeout = TimeSpan.FromSeconds(15),
});
// </shell>
// <codeact>
// CodeAct: a sandboxed Python interpreter the model can write and run code in to crunch numbers.
// It runs on Hyperlight (a micro-VM, so it needs hardware virtualization). The guest module path is
// resolved automatically from the Hyperlight.HyperlightSandbox.Guest.Python NuGet package.
using var codeAct = new HyperlightCodeActProvider(HyperlightCodeActProviderOptions.CreateForWasm(PythonGuestModule.GetModulePath()));
// </codeact>
// <create_agent>
// Turn the chat client into a HarnessAgent. On top of Post 2's file access and approvals we add the
// four "scaling" capabilities: skills (our own provider), background agents, a confined shell, and
// CodeAct.
List<AIContextProvider> contextProviders = [skillsProvider, codeAct];
AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
// File access: portfolio.csv, reports, and the confirmations vault all live under working/.
FileAccessStore = new FileSystemAgentFileStore(workingDir),
// We supply our own skills provider (file + optional Foundry), so turn off the default one.
DisableAgentSkillsProvider = true,
// Fan-out research is delegated to this background agent.
BackgroundAgents = [researchAgent],
// The confined shell, exposed as the approval-gated run_shell tool.
ShellExecutor = shell,
// Keep reading the portfolio frictionless while writes, trades, and shell commands still prompt.
ToolApprovalAgentOptions = new ToolApprovalAgentOptions
{
AutoApprovalRules = [FileAccessProvider.ReadOnlyToolsAutoApprovalRule],
},
// Start in "execute" mode for quick lookups and actions; switch any time with /mode plan.
AgentModeProviderOptions = new AgentModeProviderOptions { DefaultMode = "execute" },
// Our skills provider plus CodeAct.
AIContextProviders = contextProviders,
ChatOptions = new ChatOptions
{
Instructions = instructions,
Tools =
[
StockTools.CreateGetStockPriceTool(),
TradingTools.CreatePlaceTradeTool(),
],
Reasoning = new() { Effort = ReasoningEffort.Medium },
},
});
// </create_agent>
try
{
// <run>
// Run the interactive console session. The default planning observers already include a tool
// approval observer, so the place_trade and run_shell approval prompts are surfaced automatically.
await HarnessConsole.RunAgentAsync(
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(
agent,
planModeName: "plan",
executionModeName: "execute",
toolFormatters: ToolCallFormatter.BuildDefaultToolFormatters())],
CommandHandlers = HarnessConsoleOptions.BuildDefaultCommandHandlers(agent),
});
// </run>
}
finally
{
codeAct?.Dispose();
if (toolboxMcpClient is not null)
{
await toolboxMcpClient.DisposeAsync().ConfigureAwait(false);
}
toolboxHttpClient?.Dispose();
}
@@ -0,0 +1,80 @@
# Scaling its capabilities (Post 3) — .NET
The third runnable sample from the [**"Build your own claw and agent harness with Microsoft Agent Framework"** blog](https://devblogs.microsoft.com/agent-framework/build-your-own-claw-and-agent-harness-with-microsoft-agent-framework)
series ([Part 3 — Scaling its capabilities](https://devblogs.microsoft.com/agent-framework/agent-harness-scaling-the-claw-or-harness-capabilities/)).
It builds on Post 2's personal finance assistant and makes it *more capable* along four axes.
## What this sample demonstrates
- **Skills** — finance know-how (`valuation`, `risk-scoring`) is packaged as discoverable `SKILL.md`
files under `skills/`, which the agent loads on demand. The sample builds its own provider with
`AgentSkillsProviderBuilder.UseFileSkills([skillsDir], scriptRunner: new SubprocessScriptRunner().RunAsync)`
so the skills' Python scripts can run, and sets `DisableAgentSkillsProvider = true` to replace the
harness default. Optionally folds in centrally-managed **Foundry skills** discovered live from a
Foundry **Toolbox MCP** endpoint via `FoundrySkills.ConnectAsync(...)` + `UseMcpSkills(...)`
(opt-in; see below).
- **Shell** — a `LocalShellExecutor` confined to the trade-confirmation vault
(`working/confirmations/`) lets the agent tidy the accumulated confirmation files (reorganize into
`year/month`, rename to `YYYY-MM-DD_TICKER_BUY|SELL.txt`). `ConfineWorkingDirectory` re-anchors
every command to the vault and a `ShellPolicy` deny-list pre-filters obviously destructive
commands. Exposed as the `run_shell` tool, which prompts for approval before each command runs.
(The deny-list is a UX guardrail, not a security boundary — for hard isolation use a
`DockerShellExecutor`.)
- **CodeAct** — a `HyperlightCodeActProvider` gives the agent a sandboxed Python interpreter to
crunch portfolio numbers by writing and running code. It runs on Hyperlight (a micro-VM), so it
requires hardware virtualization. The guest module path is resolved automatically from the
`Hyperlight.HyperlightSandbox.Guest.Python` NuGet package via `PythonGuestModule.GetModulePath()`.
- **Background agents** — a lean, web-search-only `ResearchAgent` is registered via
`HarnessAgentOptions.BackgroundAgents`, exposing the `background_agents_*` tools so the main agent
can fan out per-ticker research concurrently and aggregate the findings.
## Prerequisites
1. A Microsoft Foundry project with a deployed model (e.g. `gpt-5.4`).
2. Azure CLI installed and authenticated (`az login`).
3. *(For CodeAct)* a host with hardware virtualization enabled (Hyperlight runs the Python
interpreter in a micro-VM).
## Environment variables
```bash
export FOUNDRY_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/api/projects/your-project"
# Optional (defaults to gpt-5.4)
export FOUNDRY_MODEL="gpt-5.4"
# Optional — enable centrally-managed Foundry skills (Foundry Toolbox MCP endpoint URL):
export FOUNDRY_TOOLBOX_MCP_SERVER_URL="https://your-project.services.ai.azure.com/.../toolboxes/your-toolbox/mcp?api-version=v1"
```
When `FOUNDRY_TOOLBOX_MCP_SERVER_URL` is not set, the sample runs with the local file skills only and
prints a note.
## Running
```bash
cd dotnet
dotnet run --project samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step03_ScalingCapabilities
```
## What to expect
The sample starts an interactive loop in **execute** mode (quick lookups don't need a plan). Try
these in order:
1. `Value MSFT for me.` — the agent loads the `valuation` skill and follows its instructions
(reading references and running its script).
2. `Score the risk of my portfolio.` — the agent reads `portfolio.csv` and loads the `risk-scoring`
skill.
3. `/mode plan`, then `Tidy up my trade confirmations.` — switching to plan mode first makes the
agent inspect `working/confirmations/` and propose a reorganization plan before touching anything;
once you approve it switches to execute and uses the shell to reorganize and rename the files,
**prompting you to approve** each command.
4. `Work out the total value of my portfolio.` — the agent writes and runs Python via CodeAct.
5. `Research MSFT, NVDA and SPY and summarize the latest news.` — the agent fans the tickers out to
the background research agent and aggregates the results.
6. `What's the capital of France?` — with a `financial-agent-rules` skill published to your Foundry
toolbox and Foundry skills enabled (`FOUNDRY_TOOLBOX_MCP_SERVER_URL`), the agent loads it,
recognizes the question is off-topic, and politely declines, steering you back to finance.
See the [Part 3 blog post](https://devblogs.microsoft.com/agent-framework/agent-harness-scaling-the-claw-or-harness-capabilities/)
for more on the `financial-agent-rules` skill — including the SKILL.md to publish to your Foundry toolbox.
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace ClawSample;
/// <summary>
/// Builds the background "research" agent that the main claw fans work out to.
/// </summary>
/// <remarks>
/// This sub-agent doesn't need any of the harness machinery, so it's a plain
/// <see cref="ChatClientAgent"/> with a single tool: the hosted web search. The parent claw
/// delegates a per-ticker research task to one of these and they run concurrently.
/// </remarks>
internal static class ResearchAgent
{
/// <summary>Creates a web-search-only background agent for delegated ticker research.</summary>
/// <param name="chatClient">The chat client the background agent should use.</param>
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.",
// The only tool it needs: the same hosted web search the harness would have added.
tools: [new HostedWebSearchTool()]);
}
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.Extensions.AI;
namespace ClawSample;
/// <summary>
/// A custom function tool that gives our "claw" access to (illustrative) stock prices.
/// </summary>
/// <remarks>
/// The prices and earnings figures returned here are mock data for demonstration purposes only and
/// are not real market quotes. In a real assistant you would call a market-data API instead. The
/// trailing earnings-per-share value is included so the valuation skill has something to work with.
/// </remarks>
internal static class StockTools
{
/// <summary>A delayed, illustrative stock quote, including a trailing earnings-per-share figure.</summary>
public sealed record StockQuote(string Symbol, decimal Price, decimal TrailingEps, string Currency, DateTimeOffset AsOf);
// A tiny in-memory book of (price, trailing EPS) so the sample runs without any external dependency.
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>
/// <param name="symbol">The stock ticker symbol, e.g. <c>MSFT</c> or <c>AAPL</c>.</param>
[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))
{
// Deterministic pseudo-values for unknown symbols so the sample stays self-contained.
// Derive a stable seed from the characters — string.GetHashCode() is randomized per
// process and Math.Abs(int.MinValue) throws, so neither is safe for repeatable output.
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 <see cref="AIFunction"/> wrapper used to expose the tool to the agent.</summary>
public static AIFunction CreateGetStockPriceTool() => AIFunctionFactory.Create(GetStockPrice, "get_stock_price");
}
@@ -0,0 +1,191 @@
// Copyright (c) Microsoft. All rights reserved.
// Sample subprocess-based skill script runner.
// Executes file-based skill scripts as local subprocesses.
// This is provided for demonstration purposes only.
using System.Diagnostics;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
/// <summary>
/// Executes file-based skill scripts as local subprocesses.
/// </summary>
/// <remarks>
/// This runner uses the script's absolute path and converts the arguments
/// to CLI arguments. When the LLM sends a JSON array, each element is used
/// as a positional argument. It is intended for demonstration purposes only.
/// </remarks>
internal sealed class SubprocessScriptRunner
{
/// <summary>Maximum time a skill script is allowed to run before it is terminated.</summary>
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>
/// <param name="loggerFactory">
/// Optional logger factory. When provided, script outcomes (success output, stderr, non-zero
/// exit codes, and failures) are written to the log in addition to being returned to the LLM.
/// </param>
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
{
// Windows Python installs commonly expose "python" rather than "python3".
".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)
{
// Positional CLI arguments
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 is not null && arguments.Value.ValueKind != JsonValueKind.Null && arguments.Value.ValueKind != 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.");
}
// Bound the script's lifetime: cancel after a timeout, or when the caller cancels.
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)
{
// The timeout fired (the caller did not cancel). Kill the process and report a timeout.
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)
{
// The caller cancelled: kill the process to avoid leaving orphaned subprocesses, then rethrow.
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,55 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.Extensions.AI;
namespace ClawSample;
/// <summary>
/// Sensitive "claw" tools that take real-world actions and therefore require human approval.
/// </summary>
/// <remarks>
/// These tools only simulate their effects (no orders are placed, no email is sent). They exist
/// to demonstrate how the harness gates risky actions behind an approval prompt.
/// </remarks>
internal static class TradingTools
{
// <place_trade>
/// <summary>
/// Places a (simulated) buy or sell order for a given symbol and quantity.
/// </summary>
/// <param name="symbol">The stock ticker symbol to trade, e.g. <c>MSFT</c>.</param>
/// <param name="action">Either <c>buy</c> or <c>sell</c>.</param>
/// <param name="quantity">The number of shares to trade.</param>
[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}.";
}
// </place_trade>
/// <summary>
/// Creates an approval-required <see cref="AIFunction"/> for <see cref="PlaceTrade"/>.
/// Wrapping the function in <see cref="ApprovalRequiredAIFunction"/> tells the harness to
/// surface an approval request before the function ever runs.
/// </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
@@ -1,12 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Microsoft.Extensions.AI;
namespace Harness.Shared.Console.ToolFormatters;
/// <summary>
/// Formats <c>file_memory_*</c> tool calls, showing file names and search patterns
/// with tree-view corners for save operations.
/// with tree-view corners for write and edit operations.
/// </summary>
public sealed class FileMemoryToolFormatter : ToolCallFormatter
{
@@ -16,14 +17,16 @@ public sealed class FileMemoryToolFormatter : ToolCallFormatter
/// <inheritdoc/>
public override string? FormatDetail(FunctionCallContent call) => call.Name switch
{
"file_memory_save_file" => FormatSaveFile(call),
"file_memory_read_file" => FormatStringArg(call, "fileName"),
"file_memory_delete_file" => FormatStringArg(call, "fileName"),
"file_memory_search_files" => FormatSearchFiles(call),
"file_memory_write" => FormatWriteFile(call),
"file_memory_read" => FormatStringArg(call, "fileName"),
"file_memory_delete" => FormatStringArg(call, "fileName"),
"file_memory_replace" => FormatReplaceFile(call),
"file_memory_replace_lines" => FormatReplaceLinesFile(call),
"file_memory_grep" => FormatGrep(call),
_ => null,
};
private static string? FormatSaveFile(FunctionCallContent call)
private static string? FormatWriteFile(FunctionCallContent call)
{
string? fileName = GetStringArgumentValue(call, "fileName");
string? description = GetStringArgumentValue(call, "description");
@@ -38,19 +41,60 @@ public sealed class FileMemoryToolFormatter : ToolCallFormatter
: $"\n └─ {fileName} (with description)";
}
private static string? FormatSearchFiles(FunctionCallContent call)
private static string? FormatReplaceFile(FunctionCallContent call)
{
string? fileName = GetStringArgumentValue(call, "fileName");
if (fileName is null)
{
return null;
}
bool replaceAll = string.Equals(GetStringArgumentValue(call, "replaceAll"), "true", StringComparison.OrdinalIgnoreCase);
return replaceAll
? $"\n └─ {fileName} (replace all)"
: $"\n └─ {fileName} (replace)";
}
private static string? FormatReplaceLinesFile(FunctionCallContent call)
{
string? fileName = GetStringArgumentValue(call, "fileName");
if (fileName is null)
{
return null;
}
int count = GetEditsCount(call, "edits");
return $"\n └─ {fileName} ({count} line(s))";
}
private static int GetEditsCount(FunctionCallContent call, string paramName)
{
if (call.Arguments?.TryGetValue(paramName, out object? value) == true &&
value is JsonElement je && je.ValueKind == JsonValueKind.Array)
{
return je.GetArrayLength();
}
return 0;
}
private static string? FormatGrep(FunctionCallContent call)
{
string? pattern = GetStringArgumentValue(call, "regexPattern");
string? filePattern = GetStringArgumentValue(call, "filePattern");
string? globPattern = GetStringArgumentValue(call, "globPattern");
if (pattern is null)
{
return null;
}
return string.IsNullOrEmpty(filePattern)
return string.IsNullOrEmpty(globPattern)
? $"(/{pattern}/)"
: $"(/{pattern}/ in {filePattern})";
: $"(/{pattern}/ in {globPattern})";
}
private static string? FormatStringArg(FunctionCallContent call, string paramName)
@@ -51,3 +51,11 @@ BAC, MSFT, BA
```
The parent agent will delegate each ticker lookup to the web search background agent concurrently and present the results in a table.
## Security Considerations
`BackgroundAgentsProvider` delegates work to the agents you supply — the parent sends them text input
and receives back whatever they produce. A compromised or malicious background agent could exfiltrate
data it receives, or return adversarial output designed to influence the parent agent via indirect
prompt injection once its result is retrieved. Only supply background agents you have vetted and trust
with the data the parent may pass to them.
@@ -37,7 +37,7 @@ var instructions =
You are a data analyst assistant. You have access to a folder of data files via the file_access_* tools.
## Getting started
- Start by listing available files with file_access_list_files to see what data is available.
- Start by listing available files with file_access_ls to see what data is available.
- Read the files to understand their structure and contents.
## Working with data
@@ -46,7 +46,7 @@ var instructions =
- When calculations are needed, work through them step by step and show your reasoning.
## Writing output
- When asked to produce output files (e.g., reports, summaries, filtered data), use file_access_save_file to write them.
- When asked to produce output files (e.g., reports, summaries, filtered data), use file_access_write to write them.
- Use appropriate file formats: CSV for tabular data, Markdown for reports.
- Confirm what you wrote and where.
@@ -57,3 +57,11 @@ dotnet run --project samples/02-agents/Harness/Harness_Step05_Loop
## What to Expect
The program runs the four demos in order. Each loop is executed with `RunStreamingAsync`, so output is printed live and every re-invocation of the inner agent is marked with a `--- run N ---` header (detected via a change in the streamed `ResponseId`) — this lets you see exactly when the `LoopAgent` loops. Each streamed message is prefixed with `User:` or `Agent:` based on its role, so the loop's on-behalf-of feedback messages (surfaced as `User` turns) are visually distinct from the agent's responses (`Agent`). Each demo finishes by printing its aggregated final response. Demo 4 also prints an `Auto-approving: ...` line each time the `ToolApprovalAgent` heuristic approves the `DeployService` tool call, showing how approval-aware agents integrate with the loop.
## Security Considerations
Demo 3 uses `AIJudgeLoopEvaluator`, which is an explicit opt-in to sending the original request and the
agent's latest response to a second, external judge `IChatClient` on every iteration. A compromised or
malicious judge endpoint could exfiltrate that data, or return a manipulated verdict/gap analysis that
gets fed back into the loop as feedback — a form of indirect prompt injection. Only configure a judge
client that points at a service you trust as much as the primary model.
@@ -19,3 +19,23 @@ Samples accompanying the [*Build your own agent harness or claw with Microsoft A
| --- | --- |
| [Claw_Step01_MeetYourClaw](./BuildYourOwnClaw/Claw_Step01_MeetYourClaw/README.md) | Post 1 — a minimal HarnessAgent with a custom `get_stock_price` tool, web search, and planning |
| [Claw_Step02_WorkingWithData](./BuildYourOwnClaw/Claw_Step02_WorkingWithData/README.md) | Post 2 — file access, approvals, and durable memory (file memory plus optional Foundry memory) |
| [Claw_Step03_ScalingCapabilities](./BuildYourOwnClaw/Claw_Step03_ScalingCapabilities/README.md) | Post 3 — scaling the claw with skills (plus optional Foundry skills), a confined shell, CodeAct, and background agents |
## Security Considerations
Several harness providers extend the agent's trust boundary to external systems the developer
configures — see the security notes in the individual sample READMEs (and the XML docs on the
corresponding types) before enabling them in production:
- **`BackgroundAgentsProvider`** — delegates work to developer-supplied agents (see
[Harness_Step02_Research_WithBackgroundAgents](./Harness_Step02_Research_WithBackgroundAgents/README.md)).
- **`AIJudgeLoopEvaluator`** (used by `LoopAgent`) — sends conversation content to a second, external
judge chat client (see [Harness_Step05_Loop](./Harness_Step05_Loop/README.md)).
- **`AgentSkillsProvider`** with external skill sources (e.g. `UseMcpSkills`) — loads skill content,
and potentially scripts, from a remote source (see
[AgentSkills samples](../AgentSkills/Agent_Step06_McpBasedSkills/README.md)).
- **`SummarizationCompactionStrategy`** — used for in-loop context compaction via
`HarnessAgentOptions.CompactionStrategy`, calls out to an LLM whose output becomes permanent chat
history (see [Agent_Step18_CompactionPipeline](../Agents/Agent_Step18_CompactionPipeline/README.md)).
In every case, the capability is opt-in and requires explicit configuration by the developer, who is
responsible for vetting the external service, agent, skill source, or provider before enabling it.
@@ -155,7 +155,7 @@ internal sealed class Program
try
{
await toolboxClient.DeleteToolboxAsync(name);
await toolboxClient.DeleteAsync(name);
Console.WriteLine($"Deleted existing toolbox '{name}'");
}
catch (ClientResultException ex) when (ex.Status == 404)
@@ -163,14 +163,15 @@ internal sealed class Program
// Toolbox does not exist.
}
ProjectsAgentTool webTool = ProjectsAgentTool.AsProjectTool(ResponseTool.CreateWebSearchTool());
WebSearchToolboxTool webTool = new();
ProjectsAgentTool mcpTool = ProjectsAgentTool.AsProjectTool(ResponseTool.CreateMcpTool(
serverLabel: serverLabel,
serverUri: new Uri("https://learn.microsoft.com/api/mcp"),
toolCallApprovalPolicy: new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval)));
MCPToolboxTool mcpTool = new(serverLabel)
{
ServerUri = new Uri("https://learn.microsoft.com/api/mcp"),
ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval),
};
ToolboxVersion created = (await toolboxClient.CreateToolboxVersionAsync(
ToolboxVersion created = (await toolboxClient.CreateVersionAsync(
name: name,
tools: [webTool, mcpTool],
description: "Sample toolbox combining Foundry web search with the Microsoft Learn MCP tools for the declarative InvokeFoundryToolboxMcp sample.")).Value;
@@ -7,7 +7,4 @@ SKILL_NAMES=support-style,escalation-policy
# Set to true to provision sample skills to Foundry on startup (first-run convenience).
# In production, skills are provisioned externally — leave this unset or false.
PROVISION_SAMPLE_SKILLS=true
AZURE_BEARER_TOKEN=DefaultAzureCredential
# When running outside the Foundry platform the platform-injected user-identity key is absent.
# This variable provides a fallback value for local Docker debugging only.
HOSTED_USER_ISOLATION_KEY=local-dev-user
AZURE_BEARER_TOKEN=DefaultAzureCredential
@@ -112,7 +112,6 @@ AIAgent agent = projectClient.AsAIAgent(new ChatClientAgentOptions
// Host the agent as a Foundry Hosted Agent using the Responses API.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
var app = builder.Build();
app.MapFoundryResponses();
@@ -14,9 +14,9 @@
- .env populated with FOUNDRY_PROJECT_ENDPOINT and model deployment
- Skills provisioned to Foundry (set PROVISION_SAMPLE_SKILLS=true on first run)
.NOTES
This script is for local Docker debugging only. The Foundry platform supplies the
isolation keys for every inbound request in production and the dev fallback used here
must not be enabled in production deployments.
This script is for local Docker debugging only. Running locally the container needs no user
identity: per-user isolation simply is not triggered. On the Foundry platform the caller identity
(x-agent-user-id) is supplied automatically for every request.
#>
[CmdletBinding()]
@@ -50,7 +50,6 @@ function Start-Container {
docker run -d --name $ContainerName -p ${Port}:8088 `
-e AGENT_NAME=hosted-agent-skills `
-e AZURE_BEARER_TOKEN=$bearer `
-e HOSTED_USER_ISOLATION_KEY=smoke-user `
--env-file .env `
$ImageName | Out-Host
if ($LASTEXITCODE -ne 0) { throw "docker run failed." }
@@ -70,7 +70,6 @@ AIAgent agent = new AIProjectClient(new Uri(projectEndpoint), credential)
// Host the agent as a Foundry Hosted Agent using the Responses API.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
var app = builder.Build();
app.MapFoundryResponses();
@@ -14,8 +14,7 @@ Env.TraversePath().Load();
var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
var agentName = Environment.GetEnvironmentVariable("AGENT_NAME")
?? throw new InvalidOperationException("AGENT_NAME is not set.");
var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-chat-client-agent";
var deployment = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
@@ -44,7 +43,6 @@ AIAgent agent = new AIProjectClient(projectEndpoint, credential)
// Host the agent as a Foundry Hosted Agent using the Responses API.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
var app = builder.Build();
app.MapFoundryResponses();
@@ -179,7 +179,6 @@ AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
var app = builder.Build();
app.MapFoundryResponses();
@@ -37,7 +37,6 @@ FoundryAgent agent = aiProjectClient.AsAIAgent(agentRecord);
// Host the agent as a Foundry Hosted Agent using the Responses API.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
var app = builder.Build();
app.MapFoundryResponses();
@@ -107,7 +107,6 @@ AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
var app = builder.Build();
app.MapFoundryResponses();
@@ -116,7 +116,6 @@ AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
var app = builder.Build();
app.MapFoundryResponses();
@@ -85,7 +85,6 @@ AIAgent agent = new AIProjectClient(projectEndpoint, credential)
// Host the agent as a Foundry Hosted Agent using the Responses API.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
var app = builder.Build();
app.MapFoundryResponses();
@@ -5,7 +5,4 @@ FOUNDRY_MODEL=gpt-4o
AZURE_AI_EMBEDDING_DEPLOYMENT_NAME=text-embedding-ada-002
AZURE_AI_MEMORY_STORE_ID=hosted-memory-sample
AGENT_NAME=hosted-memory-agent
AZURE_BEARER_TOKEN=DefaultAzureCredential
# When running outside the Foundry platform the platform-injected user-identity key is absent.
# This variable provides a fallback value for local Docker debugging only.
HOSTED_USER_ISOLATION_KEY=local-dev-user
AZURE_BEARER_TOKEN=DefaultAzureCredential
@@ -28,8 +28,7 @@ Env.TraversePath().Load();
var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
var agentName = Environment.GetEnvironmentVariable("AGENT_NAME")
?? throw new InvalidOperationException("AGENT_NAME is not set.");
var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-memory-agent";
var deployment = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
var embeddingDeployment = Environment.GetEnvironmentVariable("AZURE_AI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-ada-002";
var memoryStoreName = Environment.GetEnvironmentVariable("AZURE_AI_MEMORY_STORE_ID") ?? "hosted-memory-sample";
@@ -75,9 +74,15 @@ ChatClientAgent agent = projectClient.AsAIAgent(new ChatClientAgentOptions()
});
// Host the agent as a Foundry Hosted Agent using the Responses API.
//
// Per-user memory isolation comes from the platform-injected x-agent-user-id header, resolved by the
// default HostedSessionIsolationKeyProvider into the session's HostedSessionContext. This sample scopes
// memory per user via HostedFoundryMemoryProviderScopes.PerUser(), which REQUIRES that context: a
// request with no resolved user identity throws. So locally you must send an x-agent-user-id request
// header (see scripts/smoke.ps1); vary it to simulate distinct users. On the Foundry platform the
// header is always present, so no local provider registration is needed.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
var app = builder.Build();
app.MapFoundryResponses();
@@ -39,12 +39,6 @@ ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
```
For local container runs only (the platform supplies these in production):
```env
HOSTED_USER_ISOLATION_KEY=alice
```
> `.env` is gitignored. The `.env.example` template is checked in as a reference.
## How memory scoping works
@@ -56,14 +50,11 @@ HOSTED_USER_ISOLATION_KEY=alice
| Session | The handler stores the resolved value on the session as a `HostedSessionContext` on the first request, and validates it on every subsequent request that resumes the same conversation (mismatch returns 403). |
| Memory provider | The sample's `stateInitializer` reads `session.GetHostedContext().UserId` and uses it as the `FoundryMemoryProviderScope`. Memories are partitioned per user. |
When running outside the Foundry platform the header is absent. The sample registers
`DevTemporaryLocalSessionIsolationKeyProvider` (via `AddDevTemporaryLocalContributorSetup`) which
falls back to the `HOSTED_USER_ISOLATION_KEY` environment variable,
defaulting to a single `local-dev-*` bucket when it is not set.
> **Production warning.** Never register `DevTemporaryLocalSessionIsolationKeyProvider` in
> production. The Foundry platform sets the user-identity key for every inbound request, and
> client-supplied environment variables can be forged.
This sample scopes memory per user via `HostedFoundryMemoryProviderScopes.PerUser()`, which requires a
resolved user identity — a request with none throws. So locally you **must** send an `x-agent-user-id`
request header (vary it to simulate distinct users); the default `HostedSessionIsolationKeyProvider`
reads it exactly as it reads the platform-injected value. On the Foundry platform the header is always
present, so no local provider registration is needed.
## Running directly (contributors)
@@ -78,9 +69,13 @@ The agent starts on `http://localhost:8088`.
### Test it
Per-user memories require an identity. Send an `x-agent-user-id` header to scope the call to a user
(locally you set it yourself; on the platform it is set for you):
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-H "x-agent-user-id: alice" \
-d '{"input": "Hi! My name is Taylor and I am planning a hiking trip to Patagonia in November.", "model": "hosted-memory-agent"}'
```
@@ -90,6 +85,7 @@ previous call as `previous_response_id`:
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-H "x-agent-user-id: alice" \
-d '{"input": "What do you already know about my upcoming trip?", "previous_response_id": "<id>", "model": "hosted-memory-agent"}'
```
@@ -118,23 +114,22 @@ export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.az
docker run --rm -p 8088:8088 \
-e AGENT_NAME=hosted-memory-agent \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
-e HOSTED_USER_ISOLATION_KEY=alice \
--env-file .env \
hosted-memory-agent
```
### 4. Smoke test the running container
A scripted smoke test that exercises memory recall and per-user isolation across two simulated
users is provided at `scripts/smoke.ps1`. From the sample folder:
A scripted smoke test that exercises memory recall and per-user isolation is provided at
`scripts/smoke.ps1`. From the sample folder:
```powershell
pwsh ./scripts/smoke.ps1
```
The script publishes the project, builds the image, runs the container with two distinct
`HOSTED_USER_ISOLATION_KEY` values, drives a multi-turn conversation per user, asserts that each
user only sees their own memories, and exits non-zero on failure.
The script publishes the project, builds the image, runs a **single** container, and drives two users
(alice, bob) against it by varying the `x-agent-user-id` request header. It asserts that each user
only sees their own memories, and exits non-zero on failure.
## Deploying to Foundry (azd spec)
@@ -177,4 +172,4 @@ standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented sec
| **Agent definition** | Inline (`AsAIAgent(model, instructions)`) | Inline, plus `AIContextProviders = [memoryProvider]` |
| **State** | None beyond the conversation history | Per-user memories persisted in Foundry Memory |
| **Identity** | Not used | Required: `HostedSessionContext.UserId` flows into the memory scope |
| **Local dev** | `AddDevTemporaryLocalContributorSetup()` keeps requests succeeding when isolation headers are absent | Same; additionally honours `HOSTED_USER_ISOLATION_KEY` to simulate distinct users |
| **Local dev** | Works with no identity header (per-user isolation not triggered) | Requires an `x-agent-user-id` header (memory is per-user); vary it to simulate distinct users |
@@ -3,19 +3,17 @@
.SYNOPSIS
Local smoke test for the Hosted-MemoryAgent sample.
.DESCRIPTION
Publishes the sample, builds the contributor Docker image, runs the container twice with two
distinct HOSTED_USER_ISOLATION_KEY values, drives a multi-turn conversation per user via curl
invocations, and asserts that each user only sees their own remembered details.
Exits non-zero on failure.
Publishes the sample, builds the contributor Docker image, runs ONE container, and drives two
users (alice, bob) against it by varying the x-agent-user-id request header. Asserts that each
user only sees their own remembered details. Exits non-zero on failure.
Prerequisites:
- Docker
- az login (token is fetched from the host)
- .env populated with FOUNDRY_PROJECT_ENDPOINT and model deployments
.NOTES
This script is for local Docker debugging only. The Foundry platform supplies the isolation
keys for every inbound request in production and the dev fallback used here must not be
enabled in production deployments.
The x-agent-user-id header is set here only to simulate distinct users locally. On the Foundry
platform it is supplied automatically for every request.
#>
[CmdletBinding()]
@@ -44,12 +42,11 @@ Write-Host '==> Fetching bearer token ...'
$bearer = az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv
if (-not $bearer) { throw 'Failed to obtain bearer token. Run az login.' }
function Start-Container([string]$UserKey, [string]$ContainerName) {
function Start-Container([string]$ContainerName) {
docker rm -f $ContainerName 2>$null | Out-Null
docker run -d --name $ContainerName -p ${Port}:8088 `
-e AGENT_NAME=hosted-memory-agent `
-e AZURE_BEARER_TOKEN=$bearer `
-e HOSTED_USER_ISOLATION_KEY=$UserKey `
--env-file .env `
$ImageName | Out-Host
if ($LASTEXITCODE -ne 0) { throw "docker run failed for $ContainerName." }
@@ -57,11 +54,14 @@ function Start-Container([string]$UserKey, [string]$ContainerName) {
Start-Sleep -Seconds 6
}
function Invoke-Agent([string]$Prompt, [string]$PreviousResponseId = $null) {
function Invoke-Agent([string]$Prompt, [string]$UserId, [string]$PreviousResponseId = $null) {
$body = @{ input = $Prompt; model = 'hosted-memory-agent' }
if ($PreviousResponseId) { $body['previous_response_id'] = $PreviousResponseId }
$json = $body | ConvertTo-Json -Compress
$resp = Invoke-RestMethod -Method Post -Uri "http://localhost:$Port/responses" -ContentType 'application/json' -Body $json
# x-agent-user-id is the identity the Foundry platform injects in production. Sending it locally
# is how a contributor drives per-user isolation.
$headers = @{ 'x-agent-user-id' = $UserId }
$resp = Invoke-RestMethod -Method Post -Uri "http://localhost:$Port/responses" -ContentType 'application/json' -Headers $headers -Body $json
return $resp
}
@@ -80,23 +80,23 @@ function Assert-NotContains([string]$Haystack, [string]$Needle, [string]$Label)
}
try {
# One container serves BOTH users; per-user isolation is driven purely by the x-agent-user-id
# header, exactly as the Foundry platform does in production (there the platform sets it).
Start-Container -ContainerName 'hosted-memory-smoke'
Write-Host '==> Phase 1: alice teaches the agent her trip details ...'
Start-Container -UserKey 'alice' -ContainerName 'hosted-memory-smoke-alice'
$r1 = Invoke-Agent -Prompt 'Hi! My name is Taylor and I am planning a hiking trip to Patagonia in November.'
$r2 = Invoke-Agent -Prompt 'I am travelling with my sister and we love finding scenic viewpoints.' -PreviousResponseId $r1.id
$r1 = Invoke-Agent -UserId 'alice' -Prompt 'Hi! My name is Taylor and I am planning a hiking trip to Patagonia in November.'
$r2 = Invoke-Agent -UserId 'alice' -Prompt 'I am travelling with my sister and we love finding scenic viewpoints.' -PreviousResponseId $r1.id
Write-Host "==> Waiting $RecallDelaySeconds s for memory extraction ..."
Start-Sleep -Seconds $RecallDelaySeconds
$r3 = Invoke-Agent -Prompt 'What do you already know about my upcoming trip?' -PreviousResponseId $r2.id
$r3 = Invoke-Agent -UserId 'alice' -Prompt 'What do you already know about my upcoming trip?' -PreviousResponseId $r2.id
$aliceText = ($r3.output | ForEach-Object { $_.content | ForEach-Object { $_.text } }) -join ' '
Assert-Contains $aliceText 'Patagonia' 'alice recall: Patagonia'
docker rm -f hosted-memory-smoke-alice | Out-Null
Write-Host '==> Phase 2: bob starts a fresh container with a different user isolation key ...'
Start-Container -UserKey 'bob' -ContainerName 'hosted-memory-smoke-bob'
$b1 = Invoke-Agent -Prompt 'Hello, what trip am I planning?'
Write-Host '==> Phase 2: bob asks the SAME container with a different x-agent-user-id ...'
$b1 = Invoke-Agent -UserId 'bob' -Prompt 'Hello, what trip am I planning?'
$bobText = ($b1.output | ForEach-Object { $_.content | ForEach-Object { $_.text } }) -join ' '
Assert-NotContains $bobText 'Patagonia' 'bob isolation: no leak of alice memories'
@@ -104,6 +104,5 @@ try {
Write-Host '==> All smoke assertions passed.'
}
finally {
docker rm -f hosted-memory-smoke-alice 2>$null | Out-Null
docker rm -f hosted-memory-smoke-bob 2>$null | Out-Null
docker rm -f hosted-memory-smoke 2>$null | Out-Null
}
@@ -64,7 +64,6 @@ AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
var app = builder.Build();
app.MapFoundryResponses();
@@ -51,7 +51,6 @@ AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
// Host the agent as a Foundry Hosted Agent using the Responses API.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
var app = builder.Build();
app.MapFoundryResponses();
@@ -62,7 +62,7 @@ TokenCredential credential = new ChainedTokenCredential(
new DefaultAzureCredential());
// Notes on toolbox wiring — there are two ways to attach a Foundry Toolbox to an agent:
// - Server-side "baked-in" (what this sample uses): calling AddFoundryToolboxes(name)
// - Server-side "baked-in" (what this sample uses): calling AddFoundryToolboxes(credential, name)
// below registers the toolbox with the Foundry.Hosting layer, which resolves that
// toolbox's MCP tools once at startup and automatically makes them available to the
// agent on every request. The agent code does nothing per request.
@@ -94,7 +94,7 @@ builder.Services.AddFoundryResponses(agent);
// Pre-register the toolbox name so FoundryToolboxService resolves the foundry-toolbox://
// marker at request time. With FOUNDRY_PROJECT_ENDPOINT injected by the platform, startup
// MCP tools/list against the toolbox proxy is typically <100ms in-region.
builder.Services.AddFoundryToolboxes(toolboxName);
builder.Services.AddFoundryToolboxes(credential, toolboxName);
var app = builder.Build();
app.MapFoundryResponses();
@@ -8,7 +8,7 @@ Drive the agent across the auth paths with the shared [`Using-Samples/SimpleAgen
| Aspect | This sample | Existing siblings |
|---|---|---|
| Toolbox marker pattern | `FoundryAITool.CreateHostedMcpToolbox(name)` + `AddFoundryToolboxes(name)` | Same as [`Hosted-Toolbox/`](../Hosted-Toolbox/) |
| Toolbox marker pattern | `FoundryAITool.CreateHostedMcpToolbox(name)` + `AddFoundryToolboxes(credential, name)` | Same as [`Hosted-Toolbox/`](../Hosted-Toolbox/) |
| Tools per toolbox | **Three MCP tools, each with a different auth method** | `Hosted-Toolbox/`: typically one demo tool |
| Consumption | Server-side (Foundry resolves the marker) | Same |
| Client | Shared [`Using-Samples/SimpleAgent/`](../Using-Samples/SimpleAgent/) REPL, pointed at this agent | `Hosted-Toolbox/`: any client |
@@ -203,4 +203,3 @@ Inline `authorization` on a toolbox tool entry stores credentials **inside the t
- Local development against a test MCP server with a throwaway token.
For everything else use `project_connection_id` and let the platform inject credentials.
@@ -72,13 +72,12 @@ var builder = WebApplication.CreateBuilder(args);
// Register the agent and response handler
builder.Services.AddFoundryResponses(agent);
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
// Register Foundry Toolbox: connects to the MCP proxy at startup and makes tools available.
// The toolbox name must match a toolbox registered in your Foundry project.
// When FOUNDRY_PROJECT_ENDPOINT is absent (e.g., in local development without Foundry
// infrastructure), startup succeeds without error and no toolbox tools are loaded.
builder.Services.AddFoundryToolboxes(toolboxName);
builder.Services.AddFoundryToolboxes(credential, toolboxName);
var app = builder.Build();
app.MapFoundryResponses();
@@ -2,7 +2,7 @@
A hosted Foundry agent that loads tools from a single Foundry Toolbox via the AF Foundry hosting bridge.
`AddFoundryToolboxes(name)` registers a `FoundryToolboxService` that connects to the Foundry Toolboxes MCP proxy at startup, discovers the toolbox's bundled tools via `tools/list`, and makes them available to the agent on every request. The agent code does nothing per request; the toolbox is baked in on the server.
`AddFoundryToolboxes(credential, name)` registers a `FoundryToolboxService` that connects to the Foundry Toolboxes MCP proxy at startup, discovers the toolbox's bundled tools via `tools/list`, and makes them available to the agent on every request. The agent code does nothing per request; the toolbox is baked in on the server.
This is the minimal toolbox intro. For a richer walkthrough where a single toolbox bundles three MCP tools each authenticated differently, see [`Hosted-Toolbox-AuthPaths/`](../Hosted-Toolbox-AuthPaths/).
@@ -4,7 +4,7 @@ displayName: "Hosted Toolbox"
description: >
A hosted agent that loads its tools from a single Foundry Toolbox via the
AF Foundry hosting bridge. AddFoundryToolboxes(name) connects to the Foundry
AF Foundry hosting bridge. AddFoundryToolboxes(credential, name) connects to the Foundry
Toolboxes MCP proxy at startup and exposes the toolbox's bundled tools to the
agent on every request. The toolbox itself is provisioned out of band; see this
sample's README for the portal walkthrough.
@@ -91,7 +91,6 @@ AIAgent agent = new AIProjectClient(new Uri(projectEndpoint), credential)
// ── Build the host ───────────────────────────────────────────────────────────
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
var app = builder.Build();
app.MapFoundryResponses();
@@ -13,10 +13,10 @@
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<!-- AgentServer 1.0.0-beta.26 (pulled transitively via Foundry.Hosting) requires Azure.Core 1.59.0.
This sample disables transitive pinning and references Azure.Core directly, so override just
this project to the SDK-required version without moving the solution-wide central pin. -->
<PackageReference Include="Azure.Core" VersionOverride="1.59.0" />
<!-- AgentServer (pulled transitively via Foundry.Hosting) requires Azure.Core >= 1.59.0.
The solution-wide central pin is 1.60.0, which satisfies that floor, so this project
references Azure.Core directly (transitive pinning is disabled here) without a version override. -->
<PackageReference Include="Azure.Core" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
@@ -53,7 +53,6 @@ AIAgent agent = new WorkflowBuilder(frenchAgent)
// Host the workflow agent as a Foundry Hosted Agent using the Responses API.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
builder.Services.AddDevTemporaryLocalContributorSetup(); // Local Docker debugging only - must not be used in production.
var app = builder.Build();
app.MapFoundryResponses();
@@ -1,52 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.AgentServer.Responses;
using Azure.AI.AgentServer.Responses.Models;
using Microsoft.Agents.AI.Foundry.Hosting;
namespace Hosted_Shared_Contributor_Setup;
/// <summary>
/// A <see cref="HostedSessionIsolationKeyProvider"/> for local Docker debugging only.
///
/// When the Foundry platform's <c>x-agent-user-id</c> header is absent (i.e., when the container is
/// running outside the Foundry platform), the hosting layer rejects every request with a 500 because
/// the default <see cref="HostedSessionIsolationKeyProvider"/> returns null. This provider supplies a
/// fallback value from the <c>HOSTED_USER_ISOLATION_KEY</c> environment variable, defaulting to the
/// constant below when it is not set.
///
/// This should NOT be used in production. The Foundry platform sets the user id for every inbound
/// request and forging it client-side defeats the per-user partitioning. The dev fallback exists
/// solely so a contributor can <c>docker run</c> the sample on their laptop and drive a few requests
/// end to end.
/// </summary>
public sealed class DevTemporaryLocalSessionIsolationKeyProvider : HostedSessionIsolationKeyProvider
{
/// <summary>
/// Environment variable that supplies the user isolation key when the platform header is absent.
/// </summary>
public const string UserIsolationKeyEnvironmentVariable = "HOSTED_USER_ISOLATION_KEY";
/// <summary>
/// Default user isolation key used when neither the platform header nor the environment variable
/// supplies a value. All local requests collapse onto this single bucket unless overridden.
/// </summary>
public const string DefaultLocalUserIsolationKey = "local-dev-user";
/// <inheritdoc />
public override ValueTask<HostedSessionContext?> GetKeysAsync(
ResponseContext context,
CreateResponse request,
CancellationToken cancellationToken)
{
var userKey = !string.IsNullOrWhiteSpace(context?.PlatformContext?.UserIdKey)
? context!.PlatformContext!.UserIdKey
: Environment.GetEnvironmentVariable(UserIsolationKeyEnvironmentVariable);
if (string.IsNullOrWhiteSpace(userKey))
{
userKey = DefaultLocalUserIsolationKey;
}
return new ValueTask<HostedSessionContext?>(new HostedSessionContext(userKey!));
}
}

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