fbcf0619d550c2fe2df44028cdffc2ffd595bba3
1107 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fbcf0619d5 |
Python: add agent-framework-hosting-telegram channel (#5643)
* feat(hosting-telegram): add Telegram channel package New ``agent-framework-hosting-telegram`` package implementing the Telegram Bot API channel for the Hosting framework. Mounts a webhook endpoint (``POST /telegram/webhook``) and an in-process polling loop onto an ``AgentFrameworkHost`` and translates Telegram ``Update`` payloads to/from the channel-neutral ``ChannelRequest`` / ``HostedRunResult`` plumbing. Surface (re-exported from ``agent_framework_hosting_telegram``): - ``TelegramChannel`` -- concrete ``Channel`` implementation. Owns the webhook route + an optional ``getUpdates`` long-polling lifespan, parses Telegram ``Update``s into ``ChannelRequest`` (text, photo, document, voice, callback_query, …), runs the optional ``ChannelRunHook``, calls back into the ``ChannelContext`` to invoke the agent target, and posts the response back via ``sendMessage`` / ``sendChatAction`` / ``answerCallbackQuery`` on the Telegram Bot API. Honours ``DeliveryReport.include_originating`` so cross-channel pushes can target the originating Telegram chat without double-acking. - Native fields the channel doesn't lift onto ``ChannelRequest`` (e.g. ``chat.type``, ``message.message_id``, ``callback_query.data``) are attached to ``ChannelRequest.attributes`` so a ``ChannelRunHook`` can pick them up via the standard ``protocol_request=`` kwarg. - 13 unit tests covering route wiring, ``Update`` parsing across the common content shapes, hook composition, and originating vs non-originating delivery branches. Registers the package in ``python/pyproject.toml`` ``[tool.uv.sources]`` and adds the matching pyright ``executionEnvironments`` entry. Stacks on PR-2 (Hosting core); independent of PR-3 / PR-4. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(hosting-telegram): preserve in-chat ordering, ack-before-run, drain shutdown - Replace per-update task fan-out with per-chat asyncio.Queue + worker. Telegram only guarantees update ordering up to getUpdates; the previous code spawned one task per update, which broke ordering for adjacent updates in the same chat. Updates are now serialised per chat_id (so /start then "what's the weather" can't race) while different chats still process in parallel. - Webhook handler now acks (200) immediately and runs the agent in the per-chat worker. Telegram redelivers any update the webhook doesn't 200 within ~60 seconds, so a streamed agent reply that runs longer than that previously triggered a retry storm and duplicate replies. - _on_shutdown now drains everything: poll task → per-chat workers → webhook-spawned dispatcher tasks (the new ack-before-run path), then deletes the webhook + closes the HTTP client. Previously webhook tasks were not tracked at all, so an in-flight agent invocation could leak past app shutdown. - _enqueue_update extracts chat_id from message / edited_message / callback_query; updates with no resolvable chat fall back to a one-shot dispatcher task that's still tracked in _update_tasks for shutdown. - Webhook handler now also returns 400 on malformed JSON / non-object payloads instead of crashing the request. 4 new tests cover per-chat serial ordering, parallel-across-chats isolation, ack-before-run latency, and shutdown drain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(hosting): drop redundant @pytest.mark.asyncio decorators asyncio_mode = "auto" is configured in pyproject.toml across the hosting packages, so individual @pytest.mark.asyncio decorators are unnecessary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(hosting-telegram): adapt push tests to hosted run result wrapper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(hosting-telegram): add response hooks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
07717d3124 |
Python: add agent-framework-hosting-invocations channel (#5640)
* feat(hosting-invocations): add Invocations channel package New ``agent-framework-hosting-invocations`` package implementing the "Invocations" HTTP channel for the Hosting framework -- a lightweight JSON-over-HTTP shape (``POST /invocations``) for callers that want a single request/response without committing to the full OpenAI Responses envelope. Mounts onto an ``AgentFrameworkHost`` like any other channel. Surface (re-exported from ``agent_framework_hosting_invocations``): - ``InvocationsChannel`` -- concrete ``Channel`` implementation. Owns the Starlette route, parses inbound JSON into a ``ChannelRequest`` (``input`` / ``session`` / ``metadata`` / ``options``), runs the optional ``ChannelRunHook``, calls back into the ``ChannelContext`` to invoke the agent target, and returns a flat JSON envelope (or an SSE stream when ``stream=true``). - 8 unit tests covering route wiring, isolation-key passthrough, hook composition, sync vs streaming paths, and ack-only behaviour for non-originating ``DeliveryReport``s. Registers the package in ``python/pyproject.toml`` ``[tool.uv.sources]`` and adds the matching pyright ``executionEnvironments`` entry. Independent of PR-3 (Responses); both depend only on PR-2 (Hosting core). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * review: address PR-4 round 2 feedback - expand `_stream` docstring to call out the HTTP-200 + `event: error` SSE contract (status committed before generator runs; hard failures surface as the first SSE frame, not an HTTP code) - split chunked text on full-line terminators via `splitlines()` so embedded `\r` / `\r\n` no longer leak into `data:` framing on the wire, breaking EventSource consumers - on `get_final_response()` failure, emit `event: error` instead of silently swallowing — finalize is what triggers history-provider persistence on the agent side, so a 5xx / disk-full / context-provider error must reach the client - add tests covering `stream_transform_hook` (rewrite, drop, async), CRLF-in-chunk framing, and the finalize-error → no-`[DONE]` contract Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(hosting-invocations): rename stale ChatMessage docstring reference to Message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(hosting-invocations): adapt to hosted run result wrapper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(hosting-invocations): add response hooks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
7bd68f8897 |
Python: add agent-framework-hosting-responses channel (#5639)
* feat(hosting-responses): add OpenAI Responses-shaped channel package
New ``agent-framework-hosting-responses`` package implementing the
OpenAI Responses-shaped HTTP channel for the Hosting framework. Mounts
``POST /responses`` (and a ``/responses/{response_id}`` GET) onto an
``AgentFrameworkHost`` and translates the OpenAI Responses wire shape
to/from the channel-neutral ``ChannelRequest`` / ``HostedRunResult``
plumbing.
Surface (re-exported from ``agent_framework_hosting_responses``):
- ``ResponsesChannel`` -- concrete ``Channel`` implementation. Owns the
Starlette route(s), parses inbound JSON into ``ChannelRequest``, runs
the optional ``ChannelRunHook``, calls back into the
``ChannelContext`` to invoke the agent target, builds Responses
envelopes (sync JSON or SSE), and respects
``DeliveryReport.include_originating`` so cross-channel push routes
only ack to the originating Responses caller.
- The minted ``response_id`` is propagated via the host's ContextVar
machinery so storage-side history providers (e.g.
``FoundryHostedAgentHistoryProvider``) persist envelopes against the
same id the channel returns.
- 48 unit tests covering route wiring, parsing of each Responses input
shape, hook composition, sync vs streaming paths, and originating
vs non-originating delivery branches.
Registers the package in ``python/pyproject.toml`` ``[tool.uv.sources]``
and adds the matching pyright ``executionEnvironments`` entry.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* review: address PR-3 round 2 feedback
- consume IsolationKeys.chat_key from the host-bound contextvar instead
of the raw `x-agent-chat-isolation-key` header off the wire so the
host's ASGI isolation middleware (or any operator-supplied
replacement) is the authoritative point at which the caller is
authenticated and the bucket key is established
- expand `response_id_factory` docstring to call out partition
co-location vs. partition-ownership enforcement: the channel forwards
`previous_response_id` as a hint to the factory; the storage layer
validates the embedded partition against the bound user/chat
isolation keys
- on mid-stream failure, call `deliver_response` with the accumulated
text before emitting `response.failed` so host-side history /
push-channel state stays consistent with the partial deltas the
client already saw
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(hosting-responses): fix quickstart to use current Agent API
ChatAgent was renamed to Agent and ChatMessage to Message. Update the
README quickstart to use client.as_agent(...) and refresh the stale
docstring reference in _channel.py.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(hosting-responses): adapt to hosted run result wrapper
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting-responses): add response hooks
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(hosting-responses): keep instructions in chat options
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
|
|
a83a8af5a7 |
Python: refactor FoundryHostedAgentHistoryProvider onto Foundry SDK (#5637)
* refactor(foundry_hosting): build FoundryHostedAgentHistoryProvider on azure.ai.agentserver SDK
Rebuilds the Foundry hosted-agent history provider on top of
``azure.ai.agentserver``'s ``FoundryStorageProvider`` instead of the
in-house ``_HttpStorageBackend``. Splits the monolithic ``_responses.py``
into focused modules:
- ``_history_provider.py`` — new ``FoundryHostedAgentHistoryProvider``
that talks to the SDK's ``FoundryStorageProvider``, threads
``response_id`` / ``previous_response_id`` through ``ContextVar``s via
``bind_request_context``, and lifts host-bound isolation keys
(``x-agent-{user,chat}-isolation-key``) from the optional
``agent_framework_hosting`` package into a provider-local
``IsolationContext`` so the storage layer carries the correct
partition keys without channels having to know about them.
- ``_shared.py`` — extracts all SDK ``Item`` / ``OutputItem`` ↔
framework ``Message`` conversion helpers into one place so both
``_responses.py`` and the new history provider can share them.
Restores ``_convert_file_data`` for inline ``input_file`` payloads,
and the hosted-MCP routing for ``custom_tool_call_output`` items
whose ``call_id`` carries the ``mcp_*`` prefix.
- ``_ids.py`` — shared id helpers.
- ``_responses.py`` — shrinks ~700 lines, re-exports converters for
back-compat with existing tests.
- ``tests/test_history_provider.py`` — exercises the new provider
against a fake SDK backend; the host-isolation test is gated on the
optional ``agent_framework_hosting`` import.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(foundry_hosting): add local_storage_root for file-based dev history
Adds an optional `local_storage_root: str | Path | None` parameter to
`FoundryHostedAgentHistoryProvider`. When set and the provider is
running outside a Foundry Hosted Agent container, conversations are
persisted to JSONL files via `agent_framework.FileHistoryProvider`
laid out as:
{root}/{user_key or '~none'}/{chat_key or '~none'}/{session_id}.jsonl
Hosted mode (FOUNDRY_HOSTING_ENVIRONMENT set) ignores the option with a
one-time INFO log so Foundry storage always wins on the platform. The
in-memory fallback is unchanged when the option is omitted.
Path safety: isolation segments are validated against the same character
allowlist FileHistoryProvider uses for session-id stems and
base64-url-encoded with a reserved "~iso-" prefix when unsafe. "~none"
sentinel for missing keys can never collide with a real isolation key
(real keys starting with "~" are encoded). The resolved target dir is
also re-checked to be inside the configured root.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(foundry_hosting): address PR-1 review comments
- _shared.py:_capture_raw narrows `except Exception` to `except TypeError`
and emits a WARNING with traceback so the lossy fallback to a
synthesized round-trip is observable. Mirrors the reviewer suggestion.
- _history_provider.py:save_messages narrows `except Exception` to
`except FoundryStorageError` so only storage-validation failures
(4xx/5xx, opaque server errors) are swallowed. Network / TLS / auth
/ payload-builder bugs propagate so the caller can retry / alert.
Adds an instance-level `failed_writes` counter operators can poll
for silent-drop visibility.
- _history_provider.py id-stamping loop: drops the
`contextlib.suppress(AttributeError, TypeError)` around
`item.id = new_id` so SDK contract changes surface in the test
suite instead of silently corrupting the chain (the storage backend
rejects the entire `create_response` with HTTP 500 when synthetic
prefix-based ids leak through). `import contextlib` removed.
- tests:
* Unit-cover `foundry_response_id` / `foundry_response_id_factory` /
`foundry_item_id` so SDK `IdGenerator` contract changes are caught
locally.
* Cover the `save_messages` wire payload: required-by-storage fields
(`background`, `parallel_tool_calls`, `instructions`,
`agent_reference`), env-var-driven stamping (`FOUNDRY_AGENT_NAME` /
`FOUNDRY_AGENT_VERSION` / `FOUNDRY_AGENT_SESSION_ID` /
`MODEL_DEPLOYMENT_NAME` with `AZURE_AI_MODEL_DEPLOYMENT_NAME`
fallback), and the rule that `model` / `agent_session_id` /
`agent_reference.version` are omitted (not stamped to `None`) when
their env vars are unset.
* Cover the `FOUNDRY_AGENT_SESSION_ID` last-resort chain anchor on
both the get and save paths, including the prefix gate that blocks
non-`caresp_*`/`resp_*` values from reaching storage, and the
precedence rule that a host binding wins over the env.
* Replace the old `test_save_messages_swallows_backend_errors` with
two tests asserting the new contract: storage errors are swallowed
and bump `failed_writes`; everything else propagates and leaves the
counter at zero.
141 unit tests pass; mypy + pyright + ruff clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(foundry_hosting): address PR-1 round-2 review comments
- Hosted detection now delegates to AgentConfig.from_env().is_hosted so
a future Foundry SDK rename of FOUNDRY_HOSTING_ENVIRONMENT propagates
automatically; drop the local _ENV_FOUNDRY_HOSTING_ENVIRONMENT
constant.
- Drop the FOUNDRY_AGENT_SESSION_ID fallback in both get_messages and
save_messages: per the SDK it identifies the *container instance*,
not the conversation, so chaining off it would silently merge
unrelated conversations across container restarts. The host-bound
previous_response_id (set by ResponsesChannel) is the only
authoritative anchor; the env value is still stamped into the
persisted envelope's agent_session_id for operator correlation.
- Update module docstring + replace TestFoundryAgentSessionIdAnchor
with assertions for the new contract (env var ignored as anchor,
still stamped onto persisted envelope, host binding wins).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(foundry_hosting): reconcile with upstream main (#5851, #5666)
Brings the FoundryHostedAgentHistoryProvider refactor branch back into
sync with the foundry_hosting changes that have landed on upstream
main since PR-1 was opened:
* #5851 (path traversal in checkpoint storage, CWE-22).
The workflow-host code in ``_responses.py`` builds a
``FileCheckpointStorage`` from a caller-controlled ``context_id``
(``previous_response_id`` / ``conversation_id`` / ``response_id``).
Switch both call sites to route through
``_checkpoint_storage_for_context``, which rejects separators,
NUL bytes, drive letters, absolute paths, and all-dot segments,
and enforces ``is_relative_to(root)`` before any directory is
created.
* #5666 (function approval flow).
Make the SDK-Item → AF-Message conversion helpers in ``_shared.py``
async and accept an optional ``approval_storage`` keyword:
- ``_items_to_messages`` / ``_item_to_message`` /
``_item_to_message_inner``
- ``_output_items_to_messages`` / ``_output_item_to_message`` /
``_output_item_to_message_inner``
For ``mcp_approval_request`` / ``mcp_approval_response`` items the
helpers now load the original function-call Content from the
approval storage (via ``ApprovalStorage.load_approval_request``)
instead of synthesising a placeholder. This matches upstream
semantics and lets approval round-trips reconstruct the real
payload.
The ``ApprovalStorage`` Protocol moves to ``_shared.py`` so the
conversion helpers can reference it without pulling in
``_responses.py`` (which would create a circular import). The
concrete ``InMemoryFunctionApprovalStorage`` and
``FileBasedFunctionApprovalStorage`` stay in ``_responses.py``
next to the host that owns them, and re-export
``ApprovalStorage`` from ``_shared`` for compatibility.
The workflow-host streaming path passes its own
``self._approval_storage`` into ``_to_outputs`` so approval
requests are saved at emit time.
* Bump ``_history_provider.FoundryHostedAgentHistoryProvider.get_messages``
to ``await`` the now-async ``_output_items_to_messages`` call.
No public API change beyond the new keyword-only ``approval_storage``
parameter on the four conversion entry points.
Validation:
- uv run poe check-packages -P foundry_hosting (lint + pyright clean)
- uv run poe mypy -P foundry_hosting (clean)
- uv run poe test -P foundry_hosting (183 passed, 1 skipped)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
|
|
a6027a1c91 |
Python: add agent-framework-hosting core package (#5638)
* feat(hosting): add agent-framework-hosting core package
New ``agent-framework-hosting`` package implementing ADR 0026 / SPEC-002:
the channel-neutral host that lets a single ``Agent`` (or ``Workflow``)
fan out across multiple wire protocols ("channels") behind one Starlette
ASGI app.
Surface (re-exported from ``agent_framework_hosting``):
- ``AgentFrameworkHost`` — wraps a hostable target, mounts channels onto
an ASGI app, owns per-isolation-key ``AgentSession`` reuse, threads
request context (``response_id`` / ``previous_response_id``) into
context providers via an ``ExitStack`` of ``bind_request_context``
calls, and exposes an opt-in Hypercorn ``serve()`` helper (extra
``[serve]``).
- ``Channel`` protocol + ``ChannelContribution`` — the surface a channel
package implements (routes, lifespans, identity hooks, …).
- ``ChannelRequest`` / ``ChannelSession`` / ``ChannelIdentity`` /
``ChannelPush`` / ``ChannelCommand[Context]`` / ``ChannelRunHook`` /
``ChannelStreamTransformHook`` / ``DeliveryReport`` /
``HostedRunResult`` / ``ResponseTarget`` / ``ResponseTargetKind`` /
``apply_run_hook`` — channel-side dataclasses + helpers.
- ``IsolationKeys`` + ``ISOLATION_HEADER_USER`` / ``..._CHAT`` +
``get/set/reset_current_isolation_keys`` — the host's ASGI middleware
reads the ``x-agent-{user,chat}-isolation-key`` headers off each
inbound request and exposes them to the agent stack via a
``ContextVar`` so storage-side providers (e.g.
``FoundryHostedAgentHistoryProvider``) can apply per-tenant
partitioning without channels having to forward anything.
Includes 45 unit tests covering the host, channel contributions,
isolation contextvar, and shared types. Registers the package in
``python/pyproject.toml`` ``[tool.uv.sources]`` and adds the matching
pyright ``executionEnvironments`` entry for tests.
Hypercorn is an optional dependency (``[serve]`` extra); the soft import
in ``serve()`` is annotated for pyright since it isn't on the default
install.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(hosting): address PR-2 review comments
Source-code changes
- _suppress_already_consumed: narrow contract — RuntimeError now logs
at WARNING with exc_info; non-RuntimeError still logs at exception().
Docstring clarifies that any non-clean teardown is observable.
- _BoundResponseStream: add aclose() and route __await__ through
get_final_response() so the binding is always released — fixes
contextvar leak when channels abandon the stream or use the
await-the-stream convenience.
- Lifespan: aggregate startup/shutdown callback errors; every callback
runs, all failures are logged with their qualname, and the first
error is re-raised so Starlette still aborts boot.
- _build_run_kwargs: switch session-cache write to dict.setdefault so
concurrent racers cannot orphan a session if create_session ever
yields.
- _deliver_response: introduce DeliveryReport.failed for push outages
vs explicit "no link" drops; an outage no longer triggers an
originating fallback so the channel can decide degraded behaviour.
Test additions
- tests/test_isolation.py (new): full coverage of IsolationKeys, the
contextvar helpers, header constants, and end-to-end ASGI
middleware lift / reset / passthrough.
- tests/test_host.py: TestBindRequestContext, TestBoundResponseStream
(aclose / __await__ / __getattr__ forwarding / double-close
idempotency), TestWrapInputListMessages (list[Message] LAST
precedence), TestLifespanAggregation (startup + shutdown).
- tests/test_types.py: TestApplyRunHook (sync/async/None), and
TestDeliveryReport (new failed field).
- Updated test_push_exception_marks_skipped ->
test_push_exception_lands_in_failed_no_fallback to match the new
delivery contract.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(hosting): address PR-2 round-2 review comments
- Refactor workflow checkpoint restoration into shared helpers
(_restore_workflow_checkpoint for blocking; the streaming sibling
drains the rehydration stream) so the blocking and streaming paths
rehydrate identically — clarifies the previously inline _maybe_restore
by hoisting the pattern next to the blocking call site.
- Document that blocking workflow output is text-only by design;
richer modalities ride the streaming AgentResponseUpdate channel,
which preserves all content parts.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* review: address PR-4 _host.py round 2 feedback
These review comments were filed on PR-4 (#5640) but target lines that
live in the hosting-core package (PR-2 / #5638), so the fixes land here
and PR-4's stack will pick them up on rebase.
- _suppress_already_consumed: narrow the RuntimeError catch to the two
documented benign messages (`Inner stream not available`, `Event loop
is closed`); any other RuntimeError now logs at ERROR with a full
traceback so executor bugs / runner-context state errors / checkpoint
RuntimeErrors during the post-run flush no longer masquerade as
benign cleanup noise. Still no propagation (we're in an
async-generator finally during teardown) — see the docstring.
- _restore_workflow_checkpoint{,_streaming}: log a WARNING when a
non-None latest checkpoint drains to zero events, so a stale or
partially-written checkpoint_id surfaces as an operator signal
instead of a silent state-loss.
(The `deliver_response` "no destinations resolvable" vs "every
destination errored" concern raised in 3198268038 is already addressed
by the existing `failed` vs `skipped` distinction surfaced through
`DeliveryReport.failed` — see lines 1080-1102 and the
`DeliveryReport` docstring.)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(hosting): reject path-traversal patterns in checkpoint isolation_key
The host's `_resolve_checkpoint_storage` joined `request.session.isolation_key`
directly into the configured `checkpoint_location`. The key is caller-
controlled — sourced from inbound headers (`x-agent-{user,chat}-isolation-key`
injected by the Foundry runtime), from channel-supplied derivations such as
`telegram:<chat_id>` / `entra:<oid>`, or from values set by a channel
`run_hook`. A value like `../../../etc/foo` or an absolute path would let
the resulting checkpoint directory escape the configured root (CWE-22).
This matches the path-traversal class fixed upstream in #5851 for the
foundry_hosting checkpoint storage.
New `_checkpoint_path_for_isolation_key(root, isolation_key)` helper:
- Uses a denylist (not allowlist) so legitimate namespaced keys
(`telegram:42`, `entra:abc-def`) continue to pass through unmodified.
- Rejects path separators (`/`, `\`), NUL, all-dot reductions (`.`, `..`,
`...`, ...), absolute paths (`os.path.isabs`), and drive-letter prefixes
(`os.path.splitdrive` plus an explicit `^[A-Za-z]:` check so payloads
crafted on a POSIX host still fail closed if the resulting directory
ever round-trips to Windows storage).
- After joining, resolves both sides and verifies
`target.is_relative_to(root)` as defence-in-depth.
`_resolve_checkpoint_storage` now logs a WARNING and returns `None` for
invalid keys rather than crashing the request — checkpointing is best-
effort and we prefer dropping it to letting one malformed key abort an
otherwise valid agent run.
Tests:
- `TestCheckpointPathForIsolationKey` exercises the helper directly with
legitimate keys (alphanumeric, `:`-namespaced, dotted, 200-char), all
rejected traversal patterns from #5851's MSRC repro list, and
non-string input.
- `TestHostWorkflowCheckpointingPathTraversal` verifies the end-to-end
request path: a traversal key (`../escape`) and an in-key separator
(`evil/sub`) both produce a successful agent response with no files
written under `checkpoint_location`, and the traversal case logs a
WARNING citing `isolation_key`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(hosting): address PR-2 round-3 review feedback + add response hooks
Round-3 review comment fixes:
- _types.py: drop the _EMPTY_MAPPING sentinel; ChannelIdentity.attributes
uses plain dict() as the default — simpler, no extra symbol to track.
- _host.py: drop the local `import asyncio` + `from typing import cast as
_cast` inside `serve()`; rely on the module-level imports.
- _host.py: switch `_log_incoming` to structured `extra={...}` payloads
for both INFO and DEBUG so log aggregators get queryable fields.
- _host.py: delete `_flat_context_providers` and stop descending into a
`.providers` attribute. Aggregator providers (AggregateContextProvider /
ContextProviderBase) are responsible for forwarding `response_context`
to their children themselves; the host treats whatever
`agent.context_providers` exposes as the final, flat list.
- _host.py: stop collapsing agent / workflow output to text. `_invoke`
forwards `AgentResponse.messages` (and `raw_response`) on the
`HostedRunResult`. `_invoke_workflow` builds a per-event message list
via a new `_workflow_output_to_messages` helper that preserves
AgentResponse / AgentResponseUpdate / Message / Content branches and
falls back to text only for arbitrary objects.
- _host.py: `_workflow_event_to_update` carries Content payloads through
unchanged so multi-modal workflow outputs (images, function-call
metadata, ...) survive into channels.
New features (per design discussion in the PR thread):
- HostedRunResult: rebuilt around `messages: list[Message]` with
`.text` / `.contents` as projections, a `raw_response` slot for the
underlying AgentResponse, and a `replace(messages=..., raw_response=...)`
clone helper used by the delivery layer for per-destination isolation.
The `HostedRunResult(text="...")` ctor is preserved as a back-compat
shim that synthesises a single assistant text message.
- ResponseTarget: gain `echo_input: bool = False` (also exposed on
`.channel(name, *, echo_input=...)` / `.channels([...], *, echo_input=...)`).
When set, the host pushes the originating user message to each
non-originating destination before the agent reply. Channels can
filter or transform echoes via their response_hook.
- DeliveryReport: add `echoed` / `echo_failed` tuples to surface
per-destination outcomes of the new echo phase. Echo failures do not
abort the corresponding response push on the same destination.
- ChannelResponseHook + ChannelResponseContext + apply_response_hook:
duck-typed `response_hook` attribute on channels for per-destination
post-processing. Receives a clone of the HostedRunResult and a
context carrying the request, channel name, destination identity,
originating flag, and `is_echo` phase flag. Channels stay
modality-aware (text-only wires flatten via the hook; card-capable
channels render structured contents directly).
- _deliver_response: clone-before-hook fan-out so a hook mutating one
channel's payload cannot leak into another destination's view.
Tests:
- Update _FakeAgentResponse to expose `.messages` (single assistant text
message synthesised from `text`) so existing tests pass unchanged on
the new multi-modal _invoke path.
- Replace the obsolete `test_bind_descends_one_level_into_providers_attribute`
with a regression guard asserting the host does NOT descend into
`.providers` (matches new contract).
- New tests for HostedRunResult multi-modal preservation, echo_input
fan-out with success + failure, response_hook applied per destination,
per-destination mutation isolation, and is_echo phase observability.
Docs:
- spec 002: rewrite Canonical flow with the new input → run_hook → host
→ target → wrap → per-destination clone → response_hook → push
pipeline; document multi-modality contract and per-destination
cloning; add `echo_input` row to ResponseTarget table; rewrite
HostedRunResult/HostedStreamResult row; add ChannelResponseHook /
ChannelResponseContext / apply_response_hook table; log decisions
Q28 (no host-side text collapse), Q29 (duck-typed response_hook),
Q30 (opt-in `echo_input` on ResponseTarget).
- ADR 0026: add ChannelResponseHook + multi-modality bullets;
surface `echo_input` on the ResponseTarget bullet.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(hosting): drop HostedRunResult(text=...) back-compat shim; use from_text()
Pre-release cleanup — no released callers to break, so consolidate on one
canonical entry point plus a classmethod for the ergonomic
single-text-message case:
- HostedRunResult.__init__ takes ``messages`` positionally (required); no
more ``text=`` kwarg overload, no more "synthesise an empty message
when no args" path.
- New HostedRunResult.from_text(text, *, role="assistant", raw_response=None)
classmethod for the common "wrap a single text content as one message"
case (tests, channels emitting plain strings, the echo-input phase
wrapping a user's text turn).
- ``_build_echo_payload`` uses ``HostedRunResult.from_text(raw, role="user")``
for the ``str`` and fallback branches; the other branches use the plain
ctor with explicit ``Message`` lists.
- Tests rewritten to use ``from_text("reply")`` everywhere
``HostedRunResult(text="reply")`` appeared. Added an explicit
``test_from_text_role_kwarg_overrides_default`` regression guard.
- spec 002: HostedRunResult row updated to describe the
``from_text(text, *, role="assistant")`` classmethod instead of the
removed back-compat shim.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(hosting-core): reshape HostedRunResult into generic typed envelope
Replace the flattened multi-modal HostedRunResult (carrying
messages/raw_response/.text projections) with a typed generic
envelope around the target's full-fidelity output:
class HostedRunResult(Generic[TResult]):
result: TResult
session: AgentSession | None
- Agent targets produce HostedRunResult[AgentResponse]; channels
read result.messages, result.text, result.value, result.response_id,
result.usage_details directly off the underlying response.
- Workflow targets produce HostedRunResult[WorkflowRunResult];
channels iterate result.get_outputs() and inspect
result.get_final_state() themselves (the host no longer collapses
workflow outputs onto a synthesised message list).
- The echo-input phase synthesises a HostedRunResult[AgentResponse]
wrapping the user's turn so the same per-destination delivery
machinery applies.
- replace() is now {result, session} only; the host's clone is
shallow — channels that need to mutate result itself are
responsible for their own deep copy.
Rationale: the earlier shape pre-shaped target output (collapsing
workflows onto a Message list, losing per-executor outputs, final
state, and structured value affordances). Carrying the target output
unchanged keeps the host modality-agnostic, gives channel authors
static typing where they want it, and removes 30+ lines of
host-side projection helpers.
Also updates ADR 0026 + spec 002 (Q3, Q28, Q29 amended; new Q31
captures the generic-envelope decision and rationale).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(hosting-core): document echo vs response distinction for push channels
The host already encodes the echo-vs-response phase via the
underlying Message.role on the pushed HostedRunResult:
- echo phase: payload.result.messages[*].role == "user"
- response phase: payload.result.messages[*].role == "assistant"
Both pushes go through the same ChannelPush.push(identity, payload)
entry point. Channels distinguish either by inspecting role (which
works for any push-capable channel) or — when a response_hook is
wired — by branching on ChannelResponseContext.is_echo directly.
Expand the ChannelPush Protocol docstring to make this discoverable
for channel implementers (esp. chat bots that cannot impersonate
the user on their wire and need to render echoes as quoted /
prefixed blocks rather than as bot replies).
Mirror the explanation into the spec's echo_input section.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(hosting-core): fix quickstart to use current Agent API
ChatAgent was renamed to Agent and the preferred construction pattern
is client.as_agent(...). Also drop the sibling channel import so the
snippet imports only modules declared as dependencies of this package;
point readers at the sibling packages instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(hosting-core): drop redundant @pytest.mark.asyncio decorators
asyncio_mode = "auto" is configured in pyproject.toml, so individual
@pytest.mark.asyncio decorators are unnecessary.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(hosting): add authorization profiles + IdentityAllowlist seam to ADR/spec
Composes `require_link` + `allowlist` into three named profiles (open,
forced-link, allowlist) with the allowlist itself keyed on either the
channel-native id (pre-link) or a verified IdP claim (post-link), plus
`AnyOf`/`AllOf` combinators for mixed setups. Lifts the design into
an explicit host seam (`host.authorize(...)` → `AuthorizationOutcome`
of `Allowed` / `LinkRequired` / `Denied`) instead of leaving each
channel to roll its own.
Key contract bits:
- Tri-state `AllowlistDecision` (ALLOW / DENY / ABSTAIN) so claim-based
lists can ABSTAIN until claims are available without composition
silently flipping that into DENY.
- `AuthorizationContext` carries explicit `phase` + `claim_source`
so allowlists can tell pre-link from post-link without overloading
`verified_claims is None`.
- Channel-side `allowlist: ... | Literal["inherit"] | None` with an
explicit inheritance sentinel, so the host-level `default_allowlist`
is opt-out, not opt-in.
- Construction-time validator rejects silent-deny configurations
(`LinkedClaimAllowlist` without a claim source) with a typed
`ChannelConfigurationError`.
- Group-chat denial mirrors the existing `LinkChallenge` DM-redirect
pattern; only the redacted `user_message` reaches the wire,
structured `log_details` stay in telemetry.
Ships in two waves: the Protocol + `NativeIdAllowlist` + config
validator land with the next core PR ahead of the linker; the full
pipeline + `LinkedClaimAllowlist` enforcement land with the
`IdentityLinker` core PR.
Updates: ADR 0026 (summary bullet + conceptual-API table row + resolved
Q16), spec 002 (new req #22, renumbered v1 fast-follow #23..#29 and
stretch #30..#31, new "Authorization profiles and the IdentityAllowlist
seam" subsection, inbound-ownership row, resolved Q32, follow-up entry).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting): add DurableTaskRunner seam + runtime_mode auto-detect
Introduces the explicit long-running vs ephemeral runtime distinction
and a generic DurableTaskRunner Protocol that owns non-originating
push dispatch — collapsing the previous deliveries[] per-destination
state machine, SupportsDeliveryTracking provider capability, and
Foundry update_item service ask down to a single immutable
intended_targets[] write on the message.
Spec / ADR:
- New §"Runtime modes" with auto-detect markers + defaults matrix.
- Rewrites §"Delivery tracking" → §"Intended targets + durable
delivery": intent-only on the message, operational state lives in
the runner.
- New §"Durable task runner" defining DurableTaskRunner / RetryPolicy
/ TaskHandle / TaskStatus.
- Drops §SupportsDeliveryTracking and §Foundry update_item gap.
- Resolved Qs: 12, 18, 21, 26 revised; new 17/18/19 (ADR) and
33/34/35 (spec).
Code:
- New _runner.py with InProcessTaskRunner (asyncio + bounded retry,
bounded terminal-status cache, register-after-start guard,
shutdown drain).
- _host.py: runtime_mode + durable_task_runner ctor params;
auto-detect via FOUNDRY_HOSTING_ENVIRONMENT /
AZURE_FUNCTIONS_ENVIRONMENT / AWS_LAMBDA_FUNCTION_NAME;
HOSTING_PUSH_TASK_NAME handler registered eagerly so
_deliver_response can be called outside the lifespan;
_handle_push_task does echo-then-response inline per destination;
_deliver_response now schedules one task per destination via the
runner (DeliveryReport.pushed = scheduled; .failed = schedule-time
outage only).
- _types.py: new DurableTaskRunner Protocol + RetryPolicy /
TaskHandle / TaskStatus; DeliveryReport drops echoed /
echo_failed (echo outcome owned by the runner).
- __init__.py exports the new public surface.
Tests: 132 passing, 90% coverage. New test_runner.py covers
InProcessTaskRunner success/retry/terminal-failure/cancellation/
register-after-start, runtime-mode auto-detect with synthetic env,
and the warning-on-ephemeral-without-runner path. test_host.py
delivery tests use a sync runner fake for deterministic assertions
and validate the new "schedule succeeded vs runner backend
unreachable" semantics.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting): rubber-duck round-5 — strict ephemeral, codec seam, allowlist Wave-1, drop DeliveryReport
Adopts the rubber-duck-approved package of changes from the round-5
review of PR #5638 (modulo DeliveryReport.failed — the value type is
removed entirely now that durable delivery covers the failure
surface, per user direction).
Code:
- Drop DeliveryReport value type; host-internal _deliver_response
returns bool. Failure observability is now logs (in-process) /
runner backend (durable adapters).
- Strict ephemeral default: ephemeral runtime_mode with the default
in-process runner raises RuntimeError; opt-in via
allow_in_process_runner=True (warns).
- ChannelPushCodec Protocol + DurableTaskPayloadMode enum +
_validate_runner_codec_pairing so JSON-mode runners can be safely
paired with channels via codecs; _handle_push_task accepts both
object- and JSON-envelope shapes.
- ResponseTarget.identity(...) / .identities([...]) builders +
IDENTITIES kind for explicit caller-supplied recipients; field
rename identities → _target_identities (private) with a
target_identities property to resolve the classmethod collision.
- Intent-only audit: _annotate_intended_targets writes
hosting.intended_targets / skipped_targets / includes_originating /
originating_channel onto assistant messages — single immutable
write per the runner-owned operational-state model.
- InProcessTaskRunner: 2-phase drain on shutdown
(shutdown_grace_seconds, default 5.0) so a clean shutdown does not
abandon work mid-retry; payload_mode = OBJECT class-level.
- Echo idempotency: _handle_push_task tracks an echo_done cursor on
runner-owned task state so a retry that fires after the echo
phase succeeded does not double-echo.
Wave-1 authorization seam (full landing):
- New _authorization.py with AllowlistDecision tri-state,
AuthorizationContext, IdentityAllowlist Protocol, AllowAll /
NativeIdAllowlist (with async loader cache + channel-scope ABSTAIN) /
LinkedClaimAllowlist (raise-until-Wave-2) / AnyOfAllowlists /
AllOfAllowlists / CallableAllowlist built-ins, Allowed /
LinkRequired / Denied outcomes, ChannelConfigurationError.
- Host(default_allowlist=..., identity_linker=...) + per-channel
allowlist parameter with 'inherit' / None semantics.
- _validate_channel_authorization enforces all three rules at
construction: claim-source requirement, linker presence for
require_link=True (elevated from no-op — must not ship
unenforced), and NativeIdAllowlist(channel=...) typo detection.
Combinator-walking via _flatten_allowlists catches nested
misconfigs.
- host.authorize(...) for the native-id pipeline: open path returns
Allowed with auto-issued <channel>:<native_id> isolation key (or
the existing key when the identity has been seen); ABSTAIN on a
claim-required allowlist maps to
Denied(reason_code='allowlist_requires_link') until Wave 2 wires
the linker to convert it to LinkRequired.
Spec / ADR:
- docs/specs/002-python-hosting-channels.md: Wave-1 status updated
to reflect the linker-presence rule elevation and the
host.authorize landing; new sub-sections (codec contract, drain,
echo cursor); Qs 18 / 21 DeliveryReport references purged; new
resolved Qs 36–40 covering the strict-ephemeral default, codec
contract, DeliveryReport removal, echo cursor, and drain.
- docs/decisions/0026-hosting-channels.md: Q12 DeliveryReport
reference purged; Q16 updated to reflect Wave-1 landing; new
resolved Qs 20 (codec contract) + 21 (strict ephemeral / drain /
echo cursor).
Tests:
- New tests/test_authorization.py (35 cases) covering every Wave-1
built-in, the three validator rules, combinator decision
semantics, and host.authorize across open / allow / deny /
abstain-with-claim-dep / abstain-without-claim-dep paths plus
existing-key reuse and verified-claims propagation.
- tests/test_host.py: TestDeliverResponse rewritten for the bool
return + runner.scheduled-count assertions; new tests for
IDENTITIES variant + echo idempotency.
- tests/test_runner.py: strict-ephemeral now expects RuntimeError;
allow_in_process_runner opt-in tests; shutdown drain test;
payload_mode default test.
- tests/test_types.py: TestDeliveryReport removed; new
TestDurableTaskPayloadMode + TestResponseTargetIdentities.
Validation: 178 tests pass, 91% coverage, fmt + lint + pyright +
mypy clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(hosting): add mermaid flow diagrams to ADR, spec, README
Insert the 10 hosting flow diagrams reviewed in
python/.user/hosting-diagrams.md into the public docs:
- README: runtime topology (1a) + cross-link to the spec for the
richer set.
- ADR: runtime topology, channel contribution shape, and authorization
decision (1a, 1b, 3) at the end of 'Conceptual API shape'.
- Spec: all 10 diagrams — 1a/1b at the top of API Surface, 2 in
Canonical flow, 3 in Authorization profiles, 4-7 in Scenarios 6-8,
8 in Codec contract, 9 in Echo idempotency, 10 in Scenario 9.
Doc-only; no API or behaviour change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting): add opt-in disk persistence via state_dir
Long-running hosts (always-on container, single-VM bot, local dev) lose
state on every restart today. Add an opt-in disk persistence layer under
a new `state_dir` constructor parameter on `AgentFrameworkHost` that
survives process restarts without taking on a heavyweight database
dependency.
Backed by `diskcache` (installed via the new `[disk]` optional extra).
An OS-level advisory file lock guarantees single-owner semantics so two
hosts pointed at the same directory cannot double-execute scheduled
pushes.
What persists when `state_dir` is set:
- Pending durable-task records — scheduled-but-not-yet-completed pushes
replay on the next host startup via `InProcessTaskRunner.resume()`.
Records that crashed mid-attempt resume with the already-consumed
retry budget (no full-budget re-grant).
- `_session_aliases` — per-isolation-key session-id rewrites.
- `_active` — most-recently-active channel per isolation key.
- `_identities` — `ChannelIdentity` rows for fan-out targeting,
including nested mutations of the form
`self._identities[ik][channel] = identity`.
The `state_dir` parameter accepts any of:
- `None` — today's purely in-memory behaviour.
- `str` / `PathLike` — single root; host auto-creates `runner/` and
`sessions/` subfolders.
- `HostStatePaths` TypedDict / plain mapping — per-component overrides
routed to different roots. Unknown keys raise `ValueError` to surface
typos early.
Unpicklable push payloads raise `PushPayloadNotPicklable` eagerly from
`schedule()` so issues surface at the call site rather than on the
next restart. Corrupt on-disk records are quarantined-and-logged; the
runner never crashes on resume.
Live `AgentSession` objects stay in memory and are rehydrated lazily
by the history provider on the next turn.
- New modules: `_persistence.py` (lock + normalisation),
`_state_store.py` (session-bookkeeping store).
- Runner rewrite: 4-state model (`pending` / `succeeded` / `failed`
/ `cancelled`); the transient `running` state was a bug that caused
resume to skip records that crashed mid-handler.
- New tests: `test_runner_disk.py` (8 tests), `test_host_disk.py` (8
tests). 194 passed total. pyright + mypy + ruff clean.
- README: new "Optional disk persistence" section with code samples.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting): add checkpoints to state_dir + fix host docstring
Three related polish changes on top of the disk-persistence landing:
1. Extend `state_dir` to cover workflow checkpoints. Adds
`checkpoints` as a third `HostStatePaths` key. Single-path form
(`state_dir="/foo"`) now also auto-derives `/foo/checkpoints/`
for workflow targets (equivalent to passing
`checkpoint_location="/foo/checkpoints"`). The mapping form lets
workflow callers opt out by omitting the key, or route checkpoints
to a different volume.
Conflict / precedence rules:
* Explicit `checkpoint_location` always wins over the state_dir
derived path; a warning surfaces the double-config.
* Single-path `state_dir` + non-Workflow target → checkpoints path
silently ignored (no eager directory creation either).
* Mapping form with `checkpoints` + non-Workflow target → warn
(almost certainly dead config).
* Derived path with a workflow that already has its own
`checkpoint_storage` → same `RuntimeError` as the explicit
parameter triggers, so ownership stays unambiguous.
Checkpoint persistence uses `FileCheckpointStorage` from the
framework core — no extra dependency. Only `runner` and
`sessions` require the `[disk]` extra.
2. Move `AgentFrameworkHost.__init__` parameter docs from `Args:` to
`Keyword Args:` for every parameter after the `*`. Only `target`
remains under `Args:`. Brings the docstring in line with the
actual signature (the params have always been keyword-only).
3. `HostStatePaths` already existed as a TypedDict but did not cover
`checkpoints`; updated to document the new key with the same
per-attribute docstring style as `runner` / `sessions` so editors
can surface help on the keys.
Validation: 201 tests pass (was 194; +7 checkpoint integration tests
in test_host_disk.py). pyright + mypy + ruff + bandit clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting): add core IdentityLinker authorization seam
Fold the core IdentityLinker pieces into the hosting-core PR so the
authorization surface no longer has a deferred Wave-2 placeholder.
Provider-specific linkers (for example Entra OAuth helpers) can now plug
into core without core depending on an IdP SDK.
Core additions:
- Add LinkChallenge, LinkedIdentity, LinkResolution, and IdentityLinker.
IdentityLinker.resolve(identity) is a single-call decision that returns
either a linked identity with verified claims or a challenge the channel
can render.
- Enable LinkedClaimAllowlist end-to-end. It now abstains pre-link and
allows/denies post-link against verified claims, including multi-valued
claims such as groups.
- Add AuthPolicy factories for common allowlist shapes.
- Extend Allowed with verified_claims and claim_source for audit/telemetry
without requiring callers to re-derive how the decision was made.
Host behavior:
- identity_linker is now typed as IdentityLinker | None.
- authorize() supports open, native-id, forced-link, and linked-claim
profiles end-to-end.
- require_link=True resolves via the linker and returns LinkRequired when
the identity is not linked.
- claim-based allowlists use channel-emitted verified_claims when present,
or linker-resolved claims otherwise.
- authorize() remains decision-only and does not mutate _identities/_active;
identity registry writes remain on the actual request execution path.
Docs/tests:
- Remove Wave-1/Wave-2 language from core/spec/ADR surfaces touched here.
- Update the spec/ADR to describe the core linker seam and provider-specific
linker packages.
- Add authorization tests for linker challenges, linked identities, linked
claim allowlists, channel-emitted claims, AuthPolicy factories, and the
no-mutation contract.
Validation: 214 tests pass, pyright/mypy/ruff clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(hosting): add link-store path to state_dir
Identity linking introduces host-adjacent state that needs the same state_dir treatment as runner, session, and checkpoint state. Add a links component to the host state paths so applications and linker packages have a typed, discoverable persistence location.
Changes:
- Extend HostStatePaths with links and include it in state_dir normalization (state_dir/links/ for the single-path form).
- Add SupportsLinkStorePath, an optional protocol for identity linkers that accept a host-provided link-store path.
- AgentFrameworkHost now offers state_dir links to compatible linkers, warns when an explicit links path is supplied without a linker, and warns when the configured linker manages persistence directly instead of implementing SupportsLinkStorePath.
- Update README and spec text to document the link-store component and clarify that concrete linkers still own the storage format.
- Add disk-state tests for compatible, missing, and non-configurable linkers.
Validation: 217 tests pass, pyright/mypy/ruff clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
|
|
02eb9435bf |
Bump litellm from 1.83.14 to 1.84.0 in /python (#6559)
Bumps [litellm](https://github.com/BerriAI/litellm) from 1.83.14 to 1.84.0. - [Release notes](https://github.com/BerriAI/litellm/releases) - [Commits](https://github.com/BerriAI/litellm/commits/v1.84.0) --- updated-dependencies: - dependency-name: litellm dependency-version: 1.84.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
4ff952e100 |
Python: Capture context provider instructions in agent telemetry (#6515)
* Fix agent instructions telemetry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Simplify agent instructions telemetry guard Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix observability mypy cast Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
7bf2d2a6d0 |
Python: Fix harness console rendering one streamed tool call many times (#6549)
* Fix render issue for tools that are streamed in parts.
* Address PR review: missing call_id fallback, empty-mapping args, _is_complete perf
- Print call_id-less function calls as-is instead of merging under a name-derived
key (which could drop distinct unnamed calls).
- Preserve an empty {} mapping rather than coercing it to None.
- Add a structural bracket-balance gate before json.loads in _is_complete to
avoid O(n^2) re-parsing of growing streamed arguments.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
|
|
8e10c0399a |
Python: Remove unsupported as_agent function_invocation_configuration (#6520)
* Remove unsupported as_agent config parameter Fixes #6313 Remove the unsupported function_invocation_configuration parameter from BaseChatClient.as_agent(), which currently forwards an invalid kwarg into Agent.__init__(). This keeps the existing TypeError behavior for callers but changes the error source to the public API boundary, which we do not consider a breaking change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix sample --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
bce2757477 | Foundry hosted agent responses emit failed events (#6502) | ||
|
|
0db9305625 |
Python: Integrate tool approval into the harness (#6522)
* Integrate auto tool-approval feature into harness * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Rename disable_tool_approval to disable_tool_auto_approval Addresses PR review feedback that the parameter name was unclear. The flag toggles the auto/standing tool-approval middleware. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
571cae426c |
Python: Fix Azure AI Search citation URLs (#6453)
* Fix Azure AI Search citation URLs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Enrich MCP search citation metadata Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Azure AI Search citation enrichment follow-ups Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR #6453 review comments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * updated filter for paths * also updated python paths * reverted dotnet-format change --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> |
||
|
|
7e9c043c4c |
Python: Improve PR template and breaking-change label automation (#6473)
* Improve PR template and breaking-change label automation - Add a structured "Related Issue" section using GitHub closing keywords - Add a Review Guide prompt (major changes, impact, reviewer focus) with a note that the focus item is for human reviewers only - Add checklist items for issue linkage / no duplicate PRs and invert the breaking-change item (checked = not breaking) - Extend label-title-prefix to prepend [BREAKING] when the "breaking change" label is added - Add label-breaking-change workflow to apply the "breaking change" label when a PR title contains [BREAKING] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add pull-requests agent skill with dotnet/python links - Add root .github/skills/pull-requests/SKILL.md covering PR description authoring (following the PR template) and the review-comment workflow (review -> plan -> user review -> implement -> reply to all -> resolve) - Symlink the skill from python/.github/skills and dotnet/.github/skills - Reference the skill from python/AGENTS.md and dotnet/AGENTS.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fold breaking-change labeling into label-pr workflow Move the title -> 'breaking change' label logic into the existing label-pr workflow (which already applies the python/.NET labels) and drop the separate label-breaking-change workflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR title prefix review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Pin patched MessagePack for .NET restore Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revert MessagePack central pin Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Move title prefix tests out of tracked GitHub tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Exclude skill docs from CI path filters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Match skill symlinks in CI path exclusions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Exclude AGENTS docs from CI path filters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Scope title-prefix normalization to a real prefix The normalization branch in addTitlePrefix matched ^Python (no colon), so titles like "Python samples improvements" or "Pythonic refactor" were treated as already-prefixed and only re-cased, never receiving the "Python: " prefix. Scope the match to ^<prefix>:\s* so only an actual existing prefix is normalized; otherwise the prefix is prepended. Same fix applies to the .NET prefix (e.g. ".NETStandard bump"). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
d7e8d2206d |
Python: Fix Python OTel usage detail attributes (#6493)
* fix python otel usage detail attributes Map cached/read/reasoning usage detail fields to standard OTel GenAI attributes while preserving provider-specific legacy keys. Add focused coverage for direct response spans, aggregated agent spans, and provider usage parsing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * address usage detail review feedback Omit missing OpenAI Responses usage detail counts while preserving zero-valued counts. Record zero-valued token usage in OTel histograms and add regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
d7027fc1f9 |
Python: [BREAKING] Align FileAccess tools with .NET — directory discovery and recursive search (#6476)
* Align FileAccess tools with .Net; add directory discovery and recursive search * Fix choices field description: spacing, line length, grammar Addresses PR review: separate concatenated string literals with proper spacing/newlines, wrap lines under the 120-char Ruff limit, and fix "doesn't" -> "don't". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR comments --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
df0bd4da82 |
Python: Fix ollama_chat_client.py sample: pass tools via options dict (#6480)
* Fix ollama_chat_client.py sample: pass tools via options dict The sample was passing tools as a direct keyword argument to get_response(), which caused a TypeError. The tools parameter must be passed inside the options dict per the SupportsChatGetResponse protocol. Fixes #6411 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Wrap tools in a list as expected by OllamaChatClient _prepare_tools_for_ollama iterates the tools value, so it must be a list rather than a bare FunctionTool instance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
ed4ff188fc |
Python: [Breaking] Additional bug fix for declarative workflows (#6489)
* Fix declarative object parsing bug * Remove unnecessary comment * Address PR comments * Address PR comments. * Fix CI failures. * declarative action approval bugfix * Address PR comments * Inlined single use variables. |
||
|
|
1acd242550 |
Python: Add AgentLoopMiddleware for re-running agents in a loop (#6174)
* Python: Add AgentLoopMiddleware for re-running agents in a loop Add `AgentLoopMiddleware`, an `AgentMiddleware` that re-runs the wrapped agent in a loop. A single configurable class covers three common patterns, each with a convenience classmethod factory: - Ralph loop (`.ralph(...)`): no exit criteria, with feedback tracking (`record_feedback`/`progress`), progress injection (`inject_progress`), optional fresh context per iteration (`fresh_context`), and an early-stop completion signal (`is_complete`). - Predicate (`.with_predicate(...)`): loop while a `should_continue` callable returns True (e.g. paired with `todos_remaining`/`background_tasks_running`). - Judge (`.with_judge(...)`): a second chat client decides whether the original request was answered, using a `JudgeVerdict` structured-output response. The loop also auto-resolves pending function-approval / user-input requests via an `on_approval_request` callable (bounded by `max_approval_rounds`), and the next iteration's input is controlled by `next_message`. Supports both streaming and non-streaming runs. Exports `AgentLoopMiddleware`, `JudgeVerdict`, `todos_remaining`, and `background_tasks_running`. Adds tests, a sample, and docs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Refine AgentLoopMiddleware API and sample - with_judge: add criteria list with {{criteria}} templating into judge instructions plus an agent-side instruction; add fresh_context, additional judge feedback relay; default judge max_iterations. - should_continue is now required and positional; supports (bool, str|None) feedback tuples surfaced to next_message/record_feedback via feedback kwarg. - Judge forwards full multi-modal request and response messages. - Default max_iterations=10 (explicit None = unbounded); removed is_complete and Ralph terminology; ShouldContinueResult is a real TypeAlias. - Sample: stream all loops, print iteration counts via injected user-block boundaries (robust to function calling), <role>: content formatting, per-method expected output, and a looping todo sample. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Fix CI checks for AgentLoopMiddleware - Resolve pyright errors in _loop.py: drop the always-true final_result None check (the while loop always assigns it) and cast finish_reason to the AgentResponse constructor's expected type. - Apply pyupgrade --py310-plus: import TypeAlias from typing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Resolve mypy/pyright disagreement on finish_reason pyright infers AgentResponse.finish_reason as including str and rejects the direct assignment, while mypy considers a cast redundant. Drop the cast and suppress only pyright with a targeted reportArgumentType ignore, satisfying both type checkers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Add todo+judge AgentLoopMiddleware sample Add a second AgentLoopMiddleware sample that composes two criteria in one should_continue predicate: a TodoProvider check (evaluated first) and a report-style judge chat client (evaluated once todos are complete) that grades the assembled report against shared requirements. Register it in the middleware samples README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Compose todo+judge loops as two middleware Rework the todo+judge sample to compose two AgentLoopMiddleware on the agent itself (middleware=[judge_loop, todo_loop]) instead of a single hand-written predicate. The inner todos_remaining loop drafts the report todo-by-todo and the outer with_judge loop re-runs it until an editor chat client judges the report publication-ready, reusing the built-in helpers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reset session for fresh_context loops via snapshot/restore AgentLoopMiddleware.fresh_context previously only reset context.messages, so with an attached session each iteration still reloaded the local transcript or re-threaded the service-side conversation id and the model saw the accumulated history. Snapshot the session once before the loop (via to_dict) and restore it (from_dict + field copy) between iterations, so every pass starts from the pre-loop baseline. The final iteration's pass is persisted (no restore after the terminating iteration), so a subsequent agent.run continues from there. Removed the obsolete warning, updated docstrings and core AGENTS.md, and added tests: a snapshot/restore round-trip, a session-reset streaming x fresh_context x inject_progress x store matrix across multiple runs and loop iterations, and response_format parsing across the loop. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Updated samples and docstrings --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
76b2b1bf39 |
Python: Add opt-in AG-UI thread snapshot persistence and hydration (#6471)
* feat(ag-ui): add thread snapshot store primitives Key decisions:\n- Introduce an AGUIThreadSnapshot model limited to replayable messages, optional Shared State, and optional interrupt state.\n- Define AGUIThreadSnapshotStore as an async protocol keyed by explicit Snapshot Scope and AG-UI Thread id.\n- Add InMemoryAGUIThreadSnapshotStore as memory-only, latest-only, bounded local/demo/test storage; no file-backed store is introduced.\n- Require snapshot_scope_resolver whenever an endpoint is configured with a snapshot store, including pre-wrapped runners, so thread ids are not authorization boundaries.\n\nFiles changed:\n- packages/ag-ui/agent_framework_ag_ui/_snapshots.py\n- packages/ag-ui/agent_framework_ag_ui/__init__.py\n- packages/ag-ui/agent_framework_ag_ui/_agent.py\n- packages/ag-ui/agent_framework_ag_ui/_workflow.py\n- packages/ag-ui/agent_framework_ag_ui/_endpoint.py\n- packages/core/agent_framework/ag_ui/__init__.py\n- packages/core/agent_framework/ag_ui/__init__.pyi\n- packages/ag-ui/tests/ag_ui/test_snapshots.py\n- packages/ag-ui/tests/ag_ui/test_endpoint.py\n- packages/ag-ui/tests/ag_ui/test_public_exports.py\n- packages/ag-ui/AGENTS.md\n\nVerification:\n- uv run pytest packages/ag-ui/tests/ag_ui/test_snapshots.py packages/ag-ui/tests/ag_ui/test_public_exports.py packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_requires_snapshot_scope_resolver_when_store_configured packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_accepts_snapshot_store_with_scope_resolver -q\n- uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_requires_snapshot_scope_resolver_when_store_configured packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_requires_snapshot_scope_resolver_when_wrapped_runner_has_store packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_accepts_snapshot_store_with_scope_resolver -q\n- uv run poe syntax -P ag-ui -C\n- uv run poe pyright -P ag-ui\n- uv run poe syntax -P core -C\n- uv run poe pyright -P core\n- uv run poe typing -P ag-ui\n- uv run poe typing -P core\n- uv run poe test -P ag-ui\n- uv run poe check -P ag-ui\n- git diff --check\n- git diff --cached --check\n\nBlockers / next iteration:\n- No blockers. Next slice can use the store contract to capture and hydrate agent snapshots.\n- uv repeatedly refreshed azure-ai-projects in uv.lock during local runs; reverted the generated lockfile churn because this change does not alter dependencies.\n- The poe-check commit hook was skipped after manual verification because it reformatted unrelated core MCP files outside this task. * feat(ag-ui): hydrate agent threads from snapshots Key decisions: - Resolve Snapshot Scope per endpoint request and pass it to the AG-UI runner only when snapshot storage is active. - Treat empty messages with no resume payload as an agent Hydrate Request when a scoped snapshot store is configured, replaying stored Shared State and message snapshots without invoking the wrapped agent. - Save the latest replayable agent message snapshot and Shared State at normal completion under Snapshot Scope plus AG-UI Thread id; no durable or file-backed store is introduced. Files changed: - packages/ag-ui/agent_framework_ag_ui/_agent_run.py - packages/ag-ui/agent_framework_ag_ui/_endpoint.py - packages/ag-ui/agent_framework_ag_ui/_snapshots.py - packages/ag-ui/tests/ag_ui/test_endpoint.py Verification: - uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_agent_endpoint_hydrates_stored_thread_snapshot_without_invoking_agent -q - uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_agent_endpoint_hydrates_stored_thread_snapshot_without_invoking_agent packages/ag-ui/tests/ag_ui/test_endpoint.py::test_agent_endpoint_hydrates_snapshots_by_scope_and_thread -q - uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_empty_messages packages/ag-ui/tests/ag_ui/test_endpoint.py::test_agent_endpoint_hydrates_stored_thread_snapshot_without_invoking_agent packages/ag-ui/tests/ag_ui/test_endpoint.py::test_agent_endpoint_hydrates_snapshots_by_scope_and_thread -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 Blockers / next iteration: - No blockers. Next slice can reconstruct normal new-user agent turns from stored snapshots. - uv repeatedly refreshed azure-ai-projects in uv.lock during local runs; reverted the generated lockfile churn because this change does not alter dependencies. - The poe-check commit hook was skipped after manual verification because it refreshed unrelated uv.lock dependency resolution. * feat(ag-ui): reconstruct agent turns from snapshots Key decisions: - Load scoped thread snapshots for non-hydrate agent requests only when snapshot storage is active and no resume payload is present. - Rebuild prior AG-UI history from stored snapshot messages, preserving the incoming new user suffix and treating stored snapshot content as authoritative over conflicting prior client history. - Merge stored Shared State with request state overrides before schema defaults and existing state-context injection. Files changed: - packages/ag-ui/agent_framework_ag_ui/_agent_run.py - packages/ag-ui/tests/ag_ui/test_endpoint.py Verification: - uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_agent_endpoint_prepends_stored_snapshot_for_new_user_turn -q - uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_agent_endpoint_deduplicates_full_history_and_merges_fresh_state -q - uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_endpoint_empty_messages packages/ag-ui/tests/ag_ui/test_endpoint.py::test_agent_endpoint_hydrates_stored_thread_snapshot_without_invoking_agent packages/ag-ui/tests/ag_ui/test_endpoint.py::test_agent_endpoint_hydrates_snapshots_by_scope_and_thread packages/ag-ui/tests/ag_ui/test_endpoint.py::test_agent_endpoint_prepends_stored_snapshot_for_new_user_turn packages/ag-ui/tests/ag_ui/test_endpoint.py::test_agent_endpoint_deduplicates_full_history_and_merges_fresh_state -q - uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.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 check -P ag-ui - uv run poe typing -P ag-ui - git diff --check - git diff --cached --check Blockers / next iteration: - No blockers. Next slice can enable workflow AG-UI Thread Snapshot persistence and hydration. - uv repeatedly refreshed azure-ai-projects in uv.lock during local runs; reverted the generated lockfile churn because this change does not alter dependencies. - The poe-check commit hook was skipped after manual verification because it refreshes unrelated uv.lock dependency resolution. * feat(ag-ui): hydrate workflow threads from snapshots Key decisions: - Handle workflow Hydrate Requests before resolving or invoking the wrapped workflow when snapshot storage and Snapshot Scope are active. - Capture only replayable workflow protocol data: workflow-emitted state snapshots, workflow-emitted message snapshots, and synthesized messages from text/tool output. - Keep workflow snapshot capture inactive without configured persistence, and skip saving snapshots when the workflow stream emits RUN_ERROR. Files changed: - packages/ag-ui/agent_framework_ag_ui/_workflow.py - packages/ag-ui/tests/ag_ui/test_endpoint.py Verification: - uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_workflow_endpoint_hydrates_emitted_snapshots_without_invoking_workflow packages/ag-ui/tests/ag_ui/test_endpoint.py::test_workflow_endpoint_hydrates_synthesized_text_and_tool_snapshot -q - uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py -q - uv run pytest packages/ag-ui/tests/ag_ui/golden/test_scenario_workflow.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 Blockers / next iteration: - No blockers. Next slice can preserve interruption state and protect snapshots on errors across agent and workflow endpoints. - uv repeatedly refreshed azure-ai-projects in uv.lock during local runs; reverted the generated lockfile churn because this change does not alter dependencies. - The poe-check commit hook was skipped after manual verification because it refreshes unrelated uv.lock dependency resolution. * feat(ag-ui): preserve interrupted thread snapshots Key decisions: - Capture workflow RUN_FINISHED interrupt metadata in replayable AG-UI Thread Snapshots so Hydrate Requests can restore pending workflow actions without invoking or resuming the workflow. - Keep failed agent and workflow runs from replacing the last good snapshot; RUN_ERROR streams leave the previous snapshot available for hydration. - Verify interruption hydration through endpoint-level AG-UI streams for both agent and workflow wrappers, including Shared State replay and no wrapped runner invocation. Files changed: - packages/ag-ui/agent_framework_ag_ui/_workflow.py - packages/ag-ui/tests/ag_ui/test_endpoint.py Verification: - uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py::test_workflow_endpoint_hydrates_interrupted_thread_without_invoking_workflow -q - uv run pytest 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_agent_endpoint_run_error_does_not_overwrite_previous_snapshot packages/ag-ui/tests/ag_ui/test_endpoint.py::test_workflow_endpoint_hydrates_interrupted_thread_without_invoking_workflow packages/ag-ui/tests/ag_ui/test_endpoint.py::test_workflow_endpoint_run_error_does_not_overwrite_previous_snapshot -q - uv run pytest packages/ag-ui/tests/ag_ui/test_endpoint.py -q - uv run pytest packages/ag-ui/tests/ag_ui/golden/test_scenario_workflow.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 Blockers / next iteration: - No blockers. Next slice can document AG-UI Thread Snapshot security and usage. - uv repeatedly refreshed azure-ai-projects in uv.lock during local runs; reverted the generated lockfile churn because this change does not alter dependencies. - The poe-check commit hook was skipped after manual verification because it refreshes unrelated uv.lock dependency resolution. * docs(ag-ui): document thread snapshot security Key decisions: - Document AG-UI Thread Snapshot persistence as opt-in and disabled unless a snapshot_store is configured. - Place Snapshot Scope guidance next to endpoint authentication guidance, making clear that AG-UI Thread ids identify threads but do not authorize snapshot access. - Describe built-in storage as in-memory only, process-local, latest-only, and not durable production storage; durable stores remain app-owned implementations of AGUIThreadSnapshotStore. - Call out snapshot confidentiality impact and that no file-backed AG-UI snapshot store is provided. Files changed: - packages/ag-ui/README.md Verification: - uv run python scripts/check_md_code_blocks.py packages/ag-ui/README.md --no-glob - git diff --check - git diff --cached --check - commit hook without SKIP ran changed-package lint/format and AG-UI README markdown-code-lint successfully before stopping because uv.lock was modified - uv run poe markdown-code-lint (failed due existing unrelated packages/mistral/README.md missing agent_framework_mistral import resolution; changed AG-UI README blocks passed) Blockers / next iteration: - No blockers. Local issue/PRD planning artifacts remain uncommitted. - uv refreshed azure-ai-projects in uv.lock during markdown lint and the commit hook; reverted the generated lockfile churn because this documentation change does not alter dependencies. - The poe-check commit hook was skipped after manual verification because it refreshes unrelated uv.lock dependency resolution. * fix(ag-ui): harden thread snapshot persistence edge cases - Persist the completed confirm_changes turn with interrupt=None so hydration no longer replays a stale pending interrupt after the user responds; resume requests prepend stored history so the persisted thread is not truncated. - Defer endpoint default_state application to the runners when snapshot persistence is active, filling only keys missing from both the stored snapshot state and the request state so defaults never reset persisted Shared State. - Always fold the turn's output into the persisted messages snapshot even when the outbound MESSAGES_SNAPSHOT event is suppressed for predictive tools without confirmation. - Load the stored snapshot on workflow follow-up turns, reconstruct full thread history into the run input, and seed the snapshot builder with merged state so saving a new turn no longer replaces prior history. - Move snapshot message reconstruction helpers to _run_common for reuse by the workflow runner; load stored agent snapshots on resume turns for state merge. - Add endpoint regression tests for all four scenarios. * fix(ag-ui): protect snapshot history on resume and harden suffix trust - Prepend stored thread history when persisting snapshots for resume runs on both the agent and workflow paths, so a resumed interrupt no longer overwrites the stored thread with just the resume turn's output. - Filter the incoming message suffix during thread reconstruction: only user turns and tool results answering backend-issued tool calls (stored tool calls or pending interrupts) may extend authoritative history. Client-forged assistant and tool messages are dropped and logged instead of being persisted and replayed. - Close the workflow snapshot builder's tool-call group when a tool result or text message lands, so synthesized transcripts keep tool results adjacent to their tool_calls message and stay valid as provider replay history. - Export DEFAULT_MAX_THREAD_SNAPSHOTS from agent_framework_ag_ui and expose SnapshotScopeResolver through the core ag_ui facade and stub. - Add regression tests for agent and workflow resume history preservation, forged suffix rejection, builder tool-call grouping, and the export surface. * fix(ag-ui): tolerate snapshot save failures and scope workflow cache - Wrap snapshot_store.save() on both the agent and workflow paths so a transient store failure (timeout, connection refused) is logged instead of propagating. Previously a failing save converted an already-streamed successful run into RUN_ERROR, and on the workflow path emitted RUN_ERROR after RUN_FINISHED, violating the single-terminal-event invariant. The previous snapshot stays available for hydration. - Key the workflow_factory instance cache by (snapshot_scope, thread_id). The Snapshot Scope is the authorization boundary, so the same thread id under different scopes no longer shares an in-memory workflow instance. clear_thread_workflow accepts an optional snapshot_scope and clears all scopes for the thread when omitted. - Add tests: save-failure tolerance for agent and workflow endpoints, scope-isolated workflow cache, async snapshot_scope_resolver support, and in-memory store key validation errors. * fix(ci): ignore all dotnet.microsoft.com links in linkspector The existing ignore pattern only matched https://dotnet.microsoft.com/download, but Microsoft sites insert a locale segment between host and path (e.g. /en-us/download/dotnet/10.0), so localized links slip past the pattern and get checked. dotnet.microsoft.com bot-blocks CI link checkers with intermittent 403s across the whole site, which fails markdown-link-check on unrelated pull requests since linkspector scans the entire repository. Ignore the domain wholesale, matching how platform.openai.com is already handled for the same reason. A 403 from bot blocking is indistinguishable from a removed page, so the checker cannot produce a meaningful signal for this domain either way. * ag-ui: simplify raw_messages assignment and drop OrderedDict - Replace list(cast(...)) with a typed annotation for raw_messages (_agent_run.py:866) per review suggestion - Replace OrderedDict with a plain dict in InMemoryAGUIThreadSnapshotStore (_snapshots.py:136); regular dicts are insertion-order-safe since Python 3.7, so OrderedDict is unnecessary. Update _evict_oldest to use next(iter(...)) for FIFO removal instead of popitem(last=False). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback for #2458: review comment fixes --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
e7937947d9 |
Python: Bug fix for declarative workflows (#6468)
* Fix declarative object parsing bug * Remove unnecessary comment * Address PR comments * Address PR comments. * Fix CI failures. |
||
|
|
3d5421edc1 |
Python: Integrate shell tool into harness agent (#6451)
* Integrate shell tool into AgentHarness * Validate shell_executor exposes as_function() with a clear TypeError Addresses PR review feedback: a public factory should fail fast with an actionable error rather than a cryptic AttributeError when an incompatible shell_executor is supplied. Validation happens upfront, regardless of whether the client supports shell tools. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Type shell harness params via TYPE_CHECKING import Addresses PR review feedback: type shell_executor and shell_environment_provider_options instead of Any, using a TYPE_CHECKING import from agent_framework_tools.shell. The import never executes at runtime, so there is no circular dependency, and the lazy runtime import of ShellEnvironmentProvider is retained. Since ShellExecutor is a protocol without as_function(), the validated getattr result is invoked directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
df29af611c |
Python: Add tool approval middleware (#6414)
* Add Python tool approval middleware Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix tool approval restored state handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Gate hidden approvals on explicit approval responses Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Handle string inputs in approval replay scan Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Cover argument-scoped approval rules Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refine tool approval state and budgets Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix tool approval PR CI failures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revert DevUI Aspire README link change Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
4149f24791 |
Python: [Generated by SRE Agent] Fix MCP allowed_tools empty list handling (#6296)
* Fix MCP allowed_tools empty list handling When allowed_tools is set to an empty list [], the falsy check 'if not self.allowed_tools' incorrectly treats it as unconfigured (same as None), causing all tools to be exposed. Change to an explicit 'is None' check so that an empty list correctly results in no tools being allowed. Co-authored-by: Azure SRE Agent <noreply@microsoft.com> * Clarify allowed_tools docstring: None vs [] semantics Per Eduard's review on PR #6296: explicitly document that None exposes all tools and [] exposes none, across all four MCPTool / MCPStdioTool / MCPStreamableHTTPTool / MCPWebsocketTool docstrings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * allowed_tools docstring: recommend load_tools=False for full disable Per Eduard's follow-up on PR #6296: `load_tools=False` is the cleaner idiom when you don't want to expose any tools. Reframe `allowed_tools=[]` in the docstring as a runtime guard / inspection-only path and cross-reference `load_tools`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Azure SRE Agent <noreply@microsoft.com> Co-authored-by: Giles Odigwe <79032838+giles17@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
8dde9ef627 |
Python: HarnessAgent: Disable compaction when max tokens not provided (#6410)
* HarnessAgent: Disable compaction when max tokens not provided * Fix regression. * Address PR comments * Require max_output_tokens to be positive Reject max_output_tokens=0 (must be positive), mirroring max_context_window_tokens. Addresses PR review feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
93cbf6b3f0 |
Python: Parse MCP CallToolResult.structuredContent field to prevent tool results returning None (#6421)
* Parse structuredContent from MCP CallToolResult (#3313) The _parse_tool_result_from_mcp method only iterated over the content field from CallToolResult, ignoring the structuredContent field entirely. MCP servers that return JSON data via structuredContent (e.g., Power BI MCP) appeared to return None. Add handling for structuredContent: when present, serialize it as JSON text and append it to the result list. This preserves the data for the LLM while maintaining backward compatibility with existing behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Parse MCP CallToolResult.structuredContent field to prevent tool results returning None Fixes #3313 * Address review feedback: add default=str to json.dumps and remove .checkpoints/ - Add default=str to json.dumps for structuredContent serialization so non-JSON-serializable values (e.g. bytes) degrade gracefully instead of raising TypeError - Remove all .checkpoints/ runtime artifacts from the repository - Add **/.checkpoints/ to .gitignore to prevent future accidental commits - Add test for non-serializable structuredContent values Fixes #3313 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback for #3313: Python: MCP CallToolResult.structuredContent field is not parsed, causing tool results to return None --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
9a56bc9f16 |
Python: [BREAKING] Add sampling guardrails to MCP tools (#6413)
* Add sampling guardrails to MCP tools Add approval, token, and request-count controls to the MCP sampling callback used when an MCPTool is configured with a chat client. - Add `sampling_approval_callback`, `sampling_max_tokens`, and `sampling_max_requests` parameters to `MCPTool` and its `MCPStdioTool`, `MCPStreamableHTTPTool`, and `MCPWebsocketTool` subclasses, positioned directly after `client`. - Gate each server-initiated `sampling/createMessage` request behind the approval callback, which denies by default when no callback is provided. - Clamp the requested `maxTokens` to `sampling_max_tokens` and enforce a per-session request count via `sampling_max_requests`. - Log incoming sampling requests at WARNING level (counts only). - Export `SamplingApprovalCallback` from the public API. - Add tests, a sample, and documentation updates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Make sampling denial message context-aware Distinguish the deny-by-default case (no approval callback configured) from an explicit denial by a configured `sampling_approval_callback`, so the returned ErrorData message is accurate for callback-driven denials and exceptions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
3daed114ee |
Python: bump package versions for 1.8.1 release (#6420)
* Python: bump package versions for 1.8.1 release * Python: bump agent-framework-foundry-hosting for 1.8.1 release * Python: bump ag-ui and azurefunctions for 1.8.1 release * Remove incorrect agent-framework-foundry changelog entry for #6259 * Add [1.8.1] changelog compare link and update [Unreleased] base --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> |
||
|
|
383d551b86 |
Purview: Parallelize PSPC cold-cache scope refresh (#5832)
* Parallelize Purview PSPC cold cache path * Cache Purview payment-required state for scope refresh * Cache Purview payment-required state for scope refresh * Align Purview policy action dedupe and 402 caching Deduplicate combined policy actions by action and restriction action so restriction-only actions are preserved without duplicating identical entries. Cache tenant-level payment-required state from background scope refresh so subsequent calls short-circuit consistently. * .NET: Implement best-effort caching for background job scope retrieval and add unit tests for cache write failures * Purview - feat: Enhance ScopedContentProcessor to queue ContentActivityJob when no applicable scopes are found and update related tests * docs: Update purview package README and AGENTS documentation to reflect caching optimizations and policy enforcement scenarios Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
632f67b92e |
Python: [Generated by SRE Agent] docs: clarify checkpoint storage security model and deserialization trust boundaries (#6295)
* docs: clarify checkpoint storage security model and deserialization trust boundaries Add Security Model documentation sections to the checkpoint encoding and Azure Functions serialization modules explaining: - Checkpoint storage is a trusted data source requiring access controls - The RestrictedUnpickler allowlist is defense-in-depth, not a security boundary - Developer responsibilities for securing storage backends - Guidance on using allowed_types and strip_pickle_markers Co-authored-by: Azure SRE Agent <noreply@microsoft.com> * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Azure SRE Agent <noreply@microsoft.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
29cec0d27b |
Python: fix: use getattr for non-OpenAI provider response compatibility (#6270)
* fix: use getattr for non-OpenAI provider response compatibility Fixes #6234 Fixes #6235 Use getattr with None fallback for system_fingerprint and output attributes to prevent AttributeError when non-OpenAI providers return response objects without these fields. * fix: use typed variable for response output to satisfy pyright Fixes #6235 Use getattr with None fallback for the output attribute, and assign to a typed list variable before the match statement to help pyright narrow the response item types correctly. * fix: rename response_outputs to avoid name collision with case-block variable Fixes #6235 Rename outputs to response_outputs on line 1974 to avoid mypy error about conflicting variable names in the match statement's case blocks. Also use list[Any] for explicit generic type annotation. * fix: use cast(list[Any]) for response output to satisfy pyright Fixes #6235 The getattr() call returns Unknown type which pyright cannot narrow in the match statement. Use an explicit cast to list[Any]. * fix: use hasattr guard instead of getattr for response.output Fixes #6235 Using hasattr(response, 'output') and then accessing response.output directly gives pyright enough type information to verify the match statement exhaustiveness. This avoids the cast(list[Any]) approach which pyright still flagged as partially unknown. * fix: use ternary operator for response_outputs assignment Replace if-else block with ternary expression to satisfy ruff SIM108 lint rule. This fixes the Package Checks (3.11) CI failure. * fix: use ternary with cast for ruff SIM108 and pyright type safety Replace if-else block with ternary expression using cast(list[Any], ...) to satisfy: - ruff SIM108 (use ternary instead of if-else) - ruff E501 (line length < 120) - pyright type narrowing (cast preserves type info lost in ternary) All local checks pass: ruff check, ruff format, pyright, 298 tests. * fix: replace hasattr+cast with try/except to preserve pyright types --------- Co-authored-by: Tao Chen <taochen@microsoft.com> |
||
|
|
caa75f7cdd |
Python: Add Foundry Toolbox MCP skills hosted agent sample (#6363)
* Add 12_foundry_toolbox_mcp_skills hosted agent sample Demonstrates using MCPSkillsSource with a Foundry Toolbox MCP endpoint to discover and serve skills via SkillsProvider (progressive disclosure). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix env var reference in README and reuse local var in main.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Require AZURE_AI_MODEL_DEPLOYMENT_NAME and use placeholder in .env.example Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Document Toolbox MCP skills vs Foundry Skills in sample README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reference 12_foundry_toolbox_mcp_skills in parent README 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> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
cfb033e5d4 |
Python: Filter MCP tool kwargs to declared params via allowlist (#6399)
* Filter MCP tool kwargs to declared params via allowlist Previously MCPTool combined framework runtime kwargs (from FunctionInvocationContext.kwargs) with the LLM-supplied arguments and stripped only a hardcoded denylist of known framework keys before forwarding to the MCP server. Any new framework-injected kwarg leaked to the server unless the denylist was updated. Switch to an allowlist built from each tool's declared parameters (inputSchema.properties). Only declared params are forwarded; everything else is stripped. Add an `additional_tool_argument_names` constructor argument so users can opt extra names back in, globally (Sequence[str]) and/or per remote tool name (Mapping with reserved "*" global key). The existing denylist is kept as a safety net for framework-named params a server declares in its schema; explicitly opted-in extras always win. The reserved _meta handling is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address MCP allowlist review comments and fix reload arg loss - Fix pyright reportUnknownArgumentType in _load_tools (cast schema properties). - Register declared param names before the existing-tool skip guard so that tool-list reloads preserve the allowlist for already-loaded tools (previously unchanged tools silently dropped all declared args after a background reload). - Handle bare-string values in an additional_tool_argument_names mapping instead of iterating their characters. - Clarify the framework denylist comment: explicit extras override the denylist. - Make the extras-override-denylist test unambiguous (opt in a denylisted name). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
e89e745bc0 |
Python: feat(claude): bump claude-agent-sdk to 0.2.87 (#6248)
* feat(claude): bump claude-agent-sdk to 0.2.87 Upgrade claude-agent-sdk dependency from >=0.1.36,<0.1.49 to >=0.2.87,<0.3. Changes: - Bump version pin in pyproject.toml - Add 'xhigh' effort level to ClaudeAgentOptions (Opus 4.7 specific) - Expose new upstream SDK options: skills, session_id, task_budget, include_hook_events, strict_mcp_config, continue_conversation, fork_session - Add TaskBudget type import - Update uv.lock Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: lower claude-agent-sdk floor to >=0.1.36 Keep the lower bound at 0.1.36 since the 0.1→0.2 transition was additive and our code works on older versions as long as new options aren't used. This avoids forcing unnecessary upgrades on existing users. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: replace TaskBudget import with inline type for SDK compat TaskBudget was added in claude-agent-sdk 0.2.93 but does not exist in 0.2.87. Use dict[str, int] inline type instead so type checking passes against 0.2.87. Lock file pinned to 0.2.87. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
bad05a2bdc |
Python: Harness console for python (#6312)
* Add initial harness console for python * Add textual to project * Add planning and approval flows with list selector * Address PR comments * Fix list selection bug * Fix PR #6312 round 2 review comments - Escape untrusted agent text with rich.markup.escape() in observers (text_output, planning_output, reasoning_display) to prevent markup injection - Remove non-functional 'Always approve' choices from tool_approval.py (framework lacks CreateAlwaysApproveToolResponse support) - Remove textual from root pyproject.toml dev deps (sample-specific) - Add PEP 723 inline script metadata to harness_research.py - Narrow except Exception to except NoMatches in list_selection.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix build error * Fix build errors --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
7e0767a0a0 |
Python: Fix per-service-call history persistence with server-storing clients (#6310)
* Fix per-service-call history persistence with server-storing clients When an Agent set require_per_service_call_history_persistence=True together with a HistoryProvider, and the chat client stored history server-side by default (e.g. OpenAIChatClient, STORES_BY_DEFAULT=True), the external history provider was silently never persisted. Unify persistence on the per-service-call middleware: when the flag is set and a HistoryProvider exists, the middleware is always installed and owns persistence. service_stores_history now only selects middleware behavior: - service does not store: load providers and drive the function loop with a local sentinel conversation id, or - service stores: skip loading (the service owns history) and persist each service call while the real conversation id flows through. Also rationalize chat-options handling in _prepare_run_context: - _merge_options now skips None overrides and strips remaining None values, so an unset `store` is never forwarded and the service decides its own default. - Resolve `store` and `conversation_id` once from a single combined view (effective_options) instead of probing both default and runtime dicts; the auto-injection and per-service-call resolution now agree on conversation_id. Fixes #5798 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Correct as_agent() docstring: persistence is per service call, not once per run Address PR review: when the client stores history server-side, the per-service-call middleware still persists after each model call; only provider loading is skipped. The previous "persist once per run()" wording contradicted the implementation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review: docs, missing-conversation-id warning, and tests - Clarify that require_per_service_call_history_persistence is a no-op when no HistoryProvider is present (docstrings in _agents.py and _clients.py). - Warn on every service call when the client stores history server-side but returns no conversation_id, so the (uncommon) loss of cross-turn resumability cannot fail silently. - Add tests: storing client + existing conversation_id does not raise and the id propagates; two runs on the same session keep persisting with a stable service_session_id and no provider loading; storing-without-conversation-id warns per call. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
9bc7b27813 | Match AG-UI approval responses to requested arguments (#6376) | ||
|
|
6169df04cb |
Python: fix(mem0): isolate entity retrieval and correct app_id payload (#6242)
* fix(mem0): parallel memory retrieval logic and strict type compliance * fix(mem0): align parallel retrieval types for pyright and mypy * fix(mem0): handle asyncio.CancelledError in search response and update test description * fix(mem0): improve error handling for asyncio.CancelledError and update test names for clarity * fix(mem0): improve retrieval response handling |
||
|
|
dcc218dbac |
Python: feat(python): Add MCP client OTel spans per GenAI semantic conventions (#6349)
* feat(python): Add MCP client OTel spans per GenAI semantic conventions Implement MCP client spans per the OTel GenAI Semantic Conventions for MCP (https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/#client). Operations instrumented: - initialize: CLIENT span capturing MCP session setup - tools/list: CLIENT span for tool listing (per-page) - prompts/list: CLIENT span for prompt listing (per-page) - tools/call: CLIENT span (nested under execute_tool when called via FunctionTool) - prompts/get: CLIENT span Span attributes follow the MCP semantic conventions: - Required: mcp.method.name - Conditional: error.type, gen_ai.tool.name, gen_ai.prompt.name - Recommended: gen_ai.operation.name, mcp.protocol.version, mcp.session.id, network.transport, server.address, server.port Transport-specific attributes per subclass: - MCPStdioTool: network.transport=pipe - MCPStreamableHTTPTool: network.transport=tcp, network.protocol.name=http - MCPWebsocketTool: network.transport=tcp, network.protocol.name=websocket All span creation gated behind OBSERVABILITY_SETTINGS.ENABLED. Closes #3624 Closes #4697 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: simplify MCP spans — remove enrichment logic and protocol version caching - Always create nested CLIENT spans for tools/call instead of enriching the parent execute_tool span - Remove _ACTIVE_TOOL_EXECUTION_SPAN contextvar (no longer needed) - Remove enrich_span_with_mcp_attributes() helper - Remove _otel_error_type preservation in FunctionTool.invoke() - Remove _mcp_protocol_version instance variable; protocol version is only set on the initialize span where it is available Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refine copilot solution * fix: enable automatic exception recording on MCP spans Remove record_exception=False and set_status_on_exception=False from create_mcp_client_span. Let OTel handle exception recording and status setting automatically. The manual set_mcp_span_error calls for tools/call still correctly set error.type (which OTel's automatic handling doesn't touch), so tool_error is preserved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reduce number of lines * Add comment to sample * test: address PR review comments on MCP observability tests - Fix initialize test to call mocked session.initialize() and read protocolVersion from the result instead of hardcoding it - Add tools/call McpError error-path test - Add prompts/get McpError error-path test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix export error --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
9cafd7e58b |
Python: Refactor workflow as agent pending request handling (#6259)
* WIP: Refactor Workflow as agent pending request handling * WIP: debugging empty message bug * Working: Workflow as agent with function approval * Address Copilot comments * Fix mypy * Address comments and fix pipeline * Request info non function approval now becomes function call * Revert uv.lock * Fix mypy * Bump min version of azure-ai-project * Remove RequestInfoFunctionArgs * fix tests * Fix failing tests * Fix sample |
||
|
|
d5335fbeae |
Python (fix:gemini): make Gemini honor declarative outputSchema, not just JSON mode (#5893)
* fix(gemini): preserve schema response_format * fix(gemini): satisfy pyright strict in response schema extraction Cast Any-narrowed mappings to Mapping[str, Any] in the structured-output schema helpers so pyright strict no longer reports partially-unknown member, argument, and variable types. Pass response_format["format"] straight into the recursive extractor, which already guards non-mapping inputs. No behavior change. * fix(gemini): use Sequence[object] cast to satisfy both mypy and pyright The Sequence[Any] cast pyright strict needs to know the loop element type is reported as a redundant-cast by mypy, which already narrows the isinstance branch to Sequence[Any]. Cast to Sequence[object] instead: pyright gets a fully known element type and mypy no longer sees an identical-type cast. No behavior change. --------- Co-authored-by: Evan Mattson <evan.mattson@microsoft.com> |
||
|
|
bf4ad48cf2 |
Python: MCP long-running task support in Python (#6319)
* MCP long-running task support in Python * Fix pyupgrade and AGENTS.md reconnect description - pyupgrade: drop forward-reference string annotations in _mcp.py (Python 3.10+ resolves them natively now that MCPTaskOptions is defined before use). - AGENTS.md: align reconnect description with current behavior. Phase 1 (initial tools/call) does NOT retry on connection loss; raises 'connection lost; task state unknown' instead, so a server that accepted the request but lost the response cannot start the operation twice. Phase 2 (tasks/get / tasks/result) still reconnects once against the same task_id. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix bandit nosec marker for CI pipeline * Address PR feedbacks * Clarifiied comments and addressed more PR feedbacks. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
01fc518b29 |
Python: bump package versions for 1.8.0 release (#6351)
- Released cohort (core, openai, foundry, root): 1.7.0 -> 1.8.0 - agent-framework-github-copilot: promote to RC (1.0.0rc1) - agent-framework-orchestrations: rc2 -> rc3 (bug fix) - Beta/alpha packages with changes: a2a, anthropic, azurefunctions, bedrock, foundry-hosting, mistral bumped to new date stamp (260604) - Inter-package dependency bounds updated for changed packages - CHANGELOG.md and PACKAGE_STATUS.md updated Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
dbc312a78a |
Python: Fix toolbox consent flow in hosted agent (#6249)
* Fix toolbox consent flow in hosted agent * Resolve conflict * Make unused tool as comment * Fix tests |
||
|
|
6b94315161 |
Python: Add timeout parameter to FoundryAgent to fix ConnectTimeout on multi-turn conversations (#6263)
* Python: fix ConnectTimeout on multi-turn FoundryAgent conversations (#6241) Expose a `timeout` parameter on `RawFoundryAgentChatClient`, `_FoundryAgentChatClient`, `RawFoundryAgent`, `FoundryAgent`, and `RawOpenAIChatClient` so callers can override the HTTP timeout used by the underlying AsyncOpenAI client. Root cause: `RawFoundryAgentChatClient.__init__` called `project_client.get_openai_client()` without configuring any timeout, inheriting the OpenAI SDK default of `httpx.Timeout(connect=5.0)`. When connections are recycled between turns under load, the 5 s connect timeout fires and surfaces as `openai.APITimeoutError`. Fix: - `load_openai_service_settings` (`_shared.py`): accept `timeout` and include it in `client_args` for all three `AsyncOpenAI`/ `AsyncAzureOpenAI` construction paths. - `RawOpenAIChatClient.__init__` (`_chat_client.py`): accept `timeout` and forward to `load_openai_service_settings`. - `RawFoundryAgentChatClient.__init__` (`_agent.py`): accept `timeout` and set `openai_client.timeout = timeout` on the client returned by `get_openai_client()` before passing it to the base class. - `_FoundryAgentChatClient`, `RawFoundryAgent`, `FoundryAgent`: accept and propagate `timeout` through the construction chain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add timeout parameter to FoundryAgent and RawOpenAIChatClient Expose a timeout parameter on RawFoundryAgentChatClient, _FoundryAgentChatClient, RawFoundryAgent, FoundryAgent, and RawOpenAIChatClient. When provided, the value is applied to the underlying AsyncOpenAI client so that connect timeouts under load or after connection recycling can be tuned by callers. Previously, get_openai_client() was called without any timeout override, so the SDK default of httpx.Timeout(connect=5.0) was inherited and could fire on multi-turn conversations where the underlying connection is recycled between turns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Add `timeout` parameter to `FoundryAgent` to fix `ConnectTimeout` on multi-turn conversations Fixes #6241 * fix(foundry): use with_options to avoid mutating shared OpenAI client timeout (#6241) Replace direct assignment with in RawFoundryAgentChatClient.__init__. The Azure AI Projects SDK caches and returns a shared AsyncOpenAI client per AIProjectClient. Mutating its .timeout attribute leaked the override to all other code paths sharing that client (other agents, user code). with_options() returns a new client instance with the override applied, leaving the original shared client untouched. Update tests to assert with_options is called with the correct timeout and that the original shared client's timeout attribute is not mutated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(foundry): assert with_options return value flows to instance.client (#6241) The four timeout propagation tests verified that with_options was called but did not confirm that the returned (timeout-configured) client was actually stored on the instance. A silent discard of the return value would have left the tests green while the timeout had no effect. Each test now captures the constructed instance and asserts: assert <instance>.client is openai_client_mock.with_options.return_value Affected tests: - test_raw_foundry_agent_chat_client_init_applies_timeout_to_openai_client - test_raw_foundry_agent_chat_client_init_applies_timeout_with_preview_enabled - test_foundry_agent_chat_client_init_propagates_timeout - test_foundry_agent_init_propagates_timeout_to_openai_client Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
bc0e65d716 | fix: drop hosted MCP calls when reasoning is stripped (#6210) | ||
|
|
4268080c20 |
Python: Fix spurious Magentic custom manager warning (#6261)
* Fix magentic manager warning
* Use typing_extensions.Sentinel for _MISSING sentinel value
Replace the bare object() sentinel with typing_extensions.Sentinel per
PEP 661 (now final). Sentinel provides a proper name and repr
('<_MISSING>') and is the idiomatic approach going forward.
Refs #4306
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: correct Sentinel type annotation for max_stall_count param (#6261)
Use int | Sentinel for max_stall_count parameter type annotation instead
of int with cast(Any, _MISSING) to properly express that the parameter
can hold either an int or the _MISSING sentinel value. This fixes the
pyright reportUnnecessaryComparison errors caused by the types int and
Sentinel having no overlap.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Rename _MISSING sentinel to UNSET in orchestrations
The sentinel is user-visible as a default in public init signatures, so
use UNSET (no leading underscore) instead of the private _MISSING name.
Drop the now-unnecessary reportPrivateUsage ignores on the UNSET imports.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
|
|
fe08574a7c |
Python: [BREAKING] Upgrade github-copilot-sdk to v1.0.0 (stable) (#6292)
* Python: Upgrade github-copilot-sdk to v1.0.0 (stable) Upgrade agent-framework-github-copilot from github-copilot-sdk 1.0.0b2 to the stable 1.0.0 release, adapting to all breaking API changes. Source changes (_agent.py): - SubprocessConfig removed: use RuntimeConnection.for_stdio(path=...) + CopilotClient kwargs (connection, log_level, base_directory) - Import paths: copilot.generated.session_events -> copilot.session_events - Settings: copilot_home -> base_directory (env GITHUB_COPILOT_BASE_DIRECTORY) - Default deny handler: PermissionDecisionUserNotAvailable() (from copilot.generated.rpc) Test changes: - Updated imports and client-construction assertions (kwargs-based) - Permission handler tests use concrete decision types (PermissionDecisionApproveOnce, PermissionDecisionDeniedInteractivelyByUser) Sample changes: - Permission handlers use PermissionHandler.approve_all or sync approve_and_log pattern (v1.0.0 protocol v3 dispatch is incompatible with blocking input() in permission handlers) - Function approval sample uses asyncio.to_thread for interactive prompts - Simplified imports across all samples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review: scope permission handlers, widen type, add test - Shell sample: only approve kind='shell', deny others - URL sample: only approve kind='url', deny others - Use getattr() for kind-specific attributes to satisfy pyright - Widen PermissionHandlerType to accept async handlers (matches SDK) - Add test for _deny_all_permissions return value Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix validation script and strengthen test assertion - Update scripts/sample_validation/create_dynamic_workflow_executor.py to use copilot.session_events imports and PermissionHandler.approve_all - Assert isinstance(result, PermissionDecisionUserNotAvailable) instead of stringly-typed kind check Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add integration tests for GitHubCopilotAgent Add 6 integration tests mirroring .NET coverage: - Basic non-streaming response - Streaming response - Function tool invocation - Session context (multi-turn) - Session resume by ID - Shell command execution Tests require COPILOT_GITHUB_TOKEN env var (skipped otherwise). Each test cleans up its Copilot session via delete_session. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
f970a699d8 |
Python: Fix compaction message-id collisions and tool-loop summary persistence (#6299)
* Fix compaction message-id collisions and tool-loop summary persistence Fixes two bugs in the compaction strategies: - #5237: incremental group annotation assigned message ids by position within the re-annotated slice, so moving the re-annotation start back to a previous group start restarted ids at 0 and produced collisions (e.g. a user message reusing an assistant message's id), merging groups and causing tool-result compaction to wrongly exclude messages. group_messages/_ensure_message_ids now take an id_offset and guard against existing-id collisions; annotate_message_groups threads the slice start index through as the offset. - #4991: the function-invocation loop copied the message list each iteration, so summaries inserted by compaction landed in a throwaway copy and were lost across tool-loop iterations (only the persistent excluded flags survived). _prepare_messages_for_model_call now compacts the list in place when messages is a list, so inserted summaries persist. Adds regression tests (incremental id uniqueness, existing-id collision avoidance, idempotency, and tool-loop summary persistence including streaming and conversation-id modes). Also adds a summarization.py sample demonstrating SummarizationStrategy directly with a real client, and reworks advanced.py with tool-call groups and a real summarizer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Guard incremental message-id assignment against prefix-id collisions Addresses PR review on #5237: _ensure_message_ids only guarded against collisions within the re-annotated slice. A preexisting (e.g. user-supplied) id in the preserved prefix could still be reassigned in the suffix when the id was numerically out of position, merging groups across the re-annotation boundary again. group_messages/_ensure_message_ids now accept reserved_ids, and annotate_message_groups passes the preserved prefix's ids so auto-assigned suffix ids never collide across the full list. Adds a regression test reproducing the out-of-position prefix-id collision. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
f29bae8fbc |
Python: run sync tools off the event loop (#5773)
* fix: run sync tools off event loop * chore: silence harness tool marker type check |
||
|
|
c6951c21f6 |
Python: Add MCP-based skills discovery (McpSkillsSource) (#6169)
* Add MCP-based skills discovery (McpSkill, McpSkillsSource, McpSkillResource) Implement Agent Skills discovery over MCP following the SEP-2640 convention: - McpSkillsSource: reads skill://index.json to discover skills served by an MCP server - McpSkill: lazily fetches SKILL.md content via resources/read on demand - McpSkillResource: wraps MCP resource results (text and binary) - Path traversal protection in get_resource for defense in depth - Samples for Foundry Toolbox and standalone MCP skills server - Comprehensive unit tests (514 lines) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review comments: rename to MCP* convention, fix error handling and samples - Rename McpSkill/McpSkillResource/McpSkillsSource to MCPSkill/MCPSkillResource/MCPSkillsSource - Add data-URI prefix stripping for blob resource decoding - Let non-McpError exceptions propagate from get_resource() - Fix contradictory test comment - Use interactive input() in mcp_based_skill sample - Remove misleading sample output block Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Restore debug logging for McpError in get_resource() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use AzureCliCredential in Foundry toolbox skills sample for consistency Replace DefaultAzureCredential with AzureCliCredential to match the credential convention used in all other samples. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use MCPStreamableHTTPTool in MCP skills sample Replace raw mcp library imports (ClientSession, streamable_http_client) with the framework's MCPStreamableHTTPTool to keep MCP server connections consistent regardless of whether skills are enabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Branch on McpError.error.code so only not-found errors return empty Previously _try_read_index() and get_resource() swallowed every McpError as 'no skills available', making auth failures, server crashes, and connection drops indistinguishable from a server that simply has no skills. Now only two codes are treated as not-found: - -32002 (MCP-spec Resource not found) - -32601 (METHOD_NOT_FOUND — server lacks resources/read) All other McpError codes and non-McpError exceptions propagate with a warning log, surfacing real failures visibly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add tests for non-McpError and non-not-found error propagation in MCP skills Cover the re-raise branch in MCPSkill.get_resource for plain ConnectionError/TimeoutError, the generic McpError (code 0) propagation on get_resource, and TimeoutError propagation in _try_read_index. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revert "Use MCPStreamableHTTPTool in MCP skills sample" This reverts commit f31ed0ded914e094f3ac5d811997b2cefc55836b. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Introduce MCP_SKILLS experimental feature for MCP skill classes Add a separate MCP_SKILLS feature ID to ExperimentalFeature enum and use it for MCPSkillResource, MCPSkill, and MCPSkillsSource, since their promotion timeline is partly outside of our control. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |