Skip to content

Commit a94a8aa

Browse files
Nathandrake229Naman TyagiCopilot
authored
[agentserver-core] Update task client to Routines=V2Preview opt-in header (#48617)
* [agentserver] Gate resilient TaskManager on the explicit enablement flag Make the durable-response/task subsystem strictly opt-in via `set_resilient_tasks_enabled` (auto-set by `resilient_background` / `steerable_conversations`). Previously every responses host wrapped all `store=true` responses in internal resilient tasks and therefore forced the boot-time recovery scan, even for a plain host that opted into nothing. Core (_base.py): - Construct the TaskManager (and run startup recovery) ONLY when the switch is enabled. When off, no manager is installed; `get_task_manager()` raises `TaskManagerNotInitialized` and callers degrade to non-durable in-process execution. A plain host pays nothing: no manager, no task-store call. Responses: - ResilientResponseOrchestrator auto-enables the switch when `resilient_background` or `steerable_conversations` is set, so resilient deployments keep full Row 1/2/3 recovery. - _start_resilient_background swallows `TaskManagerNotInitialized` at the single outer catch and runs the handler in-process (non-durable). Removed the hosted fail-loud branch and the now-unused `_is_hosted_environment` helper. Tests/samples: - Updated the opt-in gate tests for gated construction. - Rewrote the resilient-start-failure contract test: no-manager now swallows and runs in-process (real task-store start failures with a manager present still fail loud as platform errors). - Conformance `_test_handler` enables the switch so Row 2 (resilient_background=False) still validates mark-failed-on-crash. - Invocations resilient samples (research, multiturn, langgraph) enable the switch explicitly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [agentserver] Log a startup notice when responses resilience is disabled Emit a one-time WARNING at responses host startup when the durable-response subsystem is off (store=true responses are non-durable across an ungraceful crash), and an INFO when it is enabled, so operators are not surprised by the plain non-durable default. Reflects the final resolved state (after the resilient orchestrator's resilient_background/steerable auto-enable). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [agentserver] Auto-enable resilient tasks only for resilient_background Narrow the responses auto-enable of the resilient-tasks switch to `resilient_background` only; `steerable_conversations` no longer implicitly enables the durable subsystem. Recovery is tied to resilient_background alone; a steerable host that wants durability sets resilient_background=True (or the switch explicitly). The two-switch UX is a known rough edge to smooth over post-Public-Preview. Steering conformance handler now enables the switch explicitly, since steering (multi-turn input queuing) needs the TaskManager regardless of resilient_background. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [agentserver] Address PR feedback: docs, docstring, test, revert invocations samples - Rewrite set_resilient_tasks_enabled/_enablement docs to describe the switch as the opt-in gate for TaskManager construction (not just the recovery scan), and that a declared @task no longer implicitly enables it. - Update core README + tasks-guide to show the required opt-in, and core/responses CHANGELOGs with the breaking-change note. - Fix _start_resilient_background docstring: TaskManagerNotInitialized is now swallowed (in-process fallback) regardless of hosting; only real task-start failures raise/tag platform errors. - Rewrite the swallow contract test to enter the ASGI lifespan with the switch off, assert no TaskManager is installed, and poll GET to completion. - Revert the 3 invocations resilient sample edits (unrelated api.md whitespace consistency drift); sample opt-in to be handled as a follow-up. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [agentserver] Fix contract test isolation + actionable TaskManagerNotInitialized message - The switch-off contract tests asserted get_task_manager() raises, but the process-global TaskManager singleton can leak from a prior test in a shared pytest process (CI), causing DID-NOT-RAISE. The _switch_off fixture now also snapshots/resets/restores the manager singleton (set_task_manager(None)), so the no-manager path is exercised deterministically. - Make the TaskManagerNotInitialized message actionable: it now names set_resilient_tasks_enabled(True) and the responses resilient_background=True option instead of a generic 'ensure resilient tasks are enabled'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [agentserver] Make no-manager contract tests deterministic (fix CI flake) The switch-off contract tests asserted get_task_manager() raises after entering the lifespan, but that depended on process-global manager/flag state (mutated by 1400+ other tests) and on which core build performs manager construction — making it fail deterministically in CI while passing locally. Force the no-manager condition explicitly via set_task_manager(None) inside the running lifespan so the responses SWALLOW path is exercised robustly. Core's gating of manager construction is covered separately by the core opt-in tests. Verified in a Linux (WSL) full-suite run: both tests pass among 1436 others. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [agentserver] Fix pylint R0915: extract responses startup logging to a helper The added durability-mode startup log pushed ResponsesAgentServerHost.__init__ over the 50-statement pylint limit (53/50, R0915). Move the startup config + resilience-mode logging into a module-level _log_startup_configuration() helper, called once from __init__. Also correct the resilience note to reflect that only resilient_background auto-enables the subsystem (not steerable_conversations). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [agentserver] Address review: fix ref leak, boot fail-fast, test isolation Copilot review fixes: - _RUNTIME_REFS leak: start_resilient() cached per-request refs keyed by response_id but only the resilient task body's finally evicts them. When primitive .start() raises (e.g. TaskManagerNotInitialized on the opt-out fallback), the body never runs, permanently retaining record/context/parsed/ cancel. Pop the entry on any start failure before propagating. - Ambiguous TaskManagerNotInitialized: instead of a per-request switch check (which broke non-lifespan test clients that auto-enable via resilient_background), AgentServerHost now FAILS THE LIFESPAN when resilient tasks are enabled but manager construction/startup fails - fail-fast at boot. A missing manager at request time therefore means opt-out, so the responses swallow stays unconditional (in-process non-durable fallback). - ASCII-only TaskManagerNotInitialized message (the em-dash broke latin-1 HTTP header encoding on the platform-error path). Test isolation: - Add an autouse fixture in the responses tests that snapshots/restores the process-global resilient-tasks switch + TaskManager singleton per test, so a resilient_background=True host in one test no longer leaks enabled state into later tests (was causing integration failures when run after resilient e2e). - Add a core test asserting the lifespan fails fast on boot startup failure. Verified on Linux (WSL): core optin 12, responses contract 6, and the full responses suite (minus the POSIX-only crash-harness dir) 1436 passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [agentserver] Clear TaskManager singleton when boot startup fails When resilient tasks are enabled and TaskManager.startup() raises, the manager was already installed globally via set_task_manager() but the lifespan aborts before the shutdown block, leaving a failed/partially-started manager visible through get_task_manager(). Wrap startup with best-effort shutdown + clear the singleton before re-raising. The startup-failure test now asserts the singleton is cleared (and shutdown was invoked). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update task client to Routines=V2Preview opt-in header Aligns the hosted task provider's Foundry-Features opt-in header with the Routines V1Preview -> V2Preview rename in the agentserver-persistence TypeSpec contract (azure-rest-api-specs #45520). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Assert exact Routines=V2Preview opt-in header in transport test Addresses PR review feedback: the transport conformance test sent requests but did not verify the Foundry-Features opt-in value, so it would pass with the old or a misspelled key. Extend test_request_carries_user_agent_and_request_id to assert the emitted header is exactly 'Routines=V2Preview'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Naman Tyagi <namantyagi@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c24957f commit a94a8aa

2 files changed

Lines changed: 8 additions & 2 deletions

File tree

‎sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/tasks/_client.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ def _build_default_policies(
402402
"""
403403
return [
404404
RequestIdPolicy(),
405-
HeadersPolicy(base_headers={"Foundry-Features": "Routines=V1Preview"}),
405+
HeadersPolicy(base_headers={"Foundry-Features": "Routines=V2Preview"}),
406406
UserAgentPolicy(base_user_agent=_USER_AGENT),
407407
# Retry on 5xx and the standard transient HTTP statuses; 409
408408
# is explicitly NOT in retry_on_status_codes because

‎sdk/agentserver/azure-ai-agentserver-core/tests/tasks/test_hosted_provider_transport.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ async def test_no_retry_on_409_other_body() -> None:
206206
async def test_request_carries_user_agent_and_request_id() -> None:
207207
"""SC-017(c)(d)(e): each request carries Authorization (via the
208208
bearer-token policy), a User-Agent prefixed with the sdk moniker,
209-
and an x-ms-client-request-id."""
209+
an x-ms-client-request-id, and the Routines preview opt-in header."""
210210

211211
transport = FakeAsyncHttpTransport(
212212
[
@@ -234,6 +234,12 @@ async def test_request_carries_user_agent_and_request_id() -> None:
234234
# x-ms-client-request-id
235235
request_id = req.headers.get("x-ms-client-request-id") or req.headers.get("X-MS-Client-Request-Id")
236236
assert request_id, f"x-ms-client-request-id header missing from request; got " f"headers={req.headers!r}"
237+
# Foundry-Features opt-in: must be exactly the Routines V2 preview key so the
238+
# request targets the agentserver-persistence contract the client is built against.
239+
foundry_features = req.headers.get("Foundry-Features") or req.headers.get("foundry-features")
240+
assert foundry_features == "Routines=V2Preview", (
241+
f"Foundry-Features opt-in header must be exactly 'Routines=V2Preview'; got {foundry_features!r}"
242+
)
237243

238244

239245
# --------------------------------------------------------------------- #

0 commit comments

Comments
 (0)