Skip to content

feat(agentserver-responses): resilient/durable responses orchestration (1.0.0b10) - #47275

Merged
Nathandrake229 merged 513 commits into
mainfrom
feature/agentserver-responses-spec016
Jul 30, 2026
Merged

feat(agentserver-responses): resilient/durable responses orchestration (1.0.0b10)#47275
Nathandrake229 merged 513 commits into
mainfrom
feature/agentserver-responses-spec016

Conversation

@RaviPidaparthi

@RaviPidaparthi RaviPidaparthi commented Jun 2, 2026

Copy link
Copy Markdown
Member

Summary

Launches durable / resilient orchestration for azure-ai-agentserver-responses (1.0.0b10). This is the responses-package half of the spec 015/016 durability work, split out of #46997 (core + invocations, now merged to main). This branch has been re-framed on top of merged main, so the diff is scoped to the responses package only.

What's included

  • Resilient background responsesResponsesServerOptions(resilient_background=True) makes store=true, background=true responses survive process crashes by persisting handler progress and re-invoking the registered handler on the next process start when a prior attempt didn't reach a terminal event.
  • Steerable conversationsResponsesServerOptions(steerable_conversations=True) lets clients post a new turn on an in-flight conversation; the running handler is woken via the cancellation signal (context.pending_input_count > 0), drains the queued input on a fresh invocation, and turns are linked in a stable conversation chain.
  • ResponseContext resilience + steering surface — flat per-invocation fields: is_recovery, is_steered_turn, pending_input_count, and conversation_chain_id (native cchain_* / rchain_* id convention; task_id == conversation_chain_id).
  • Developer checkpointsyield stream.checkpoint() persists a response snapshot at a developer-chosen boundary; context.persisted_response seeds the stream on recovery (one-OutputItem-per-phase recovery pattern).
  • internal_metadata — single-turn, platform-internal metadata on items/response, persisted for recovery and always stripped before any client-facing payload.
  • context.conversation_chain_metadata — cross-turn, named-scope, explicit-flush() resilient metadata typed by the public ConversationChainMetadataNamespace protocol.
  • await context.exit_for_recovery() — uniform graceful-shutdown recovery primitive across all handler shapes.
  • Stream recovery — SSE events persisted incrementally; clients resume with GET /responses/{id}?stream=true&starting_after=<event_id>.
  • Response acceptor hook@app.response_acceptor customizes the response shape returned when a turn is queued behind an active steerable conversation.
  • StorageFileResponseStore (default local-development store) exported from azure.ai.agentserver.responses; typed ResponseAlreadyExistsError for idempotent-create on recovery.
  • Durability contract test suite, durable samples 17–22, and the accompanying e2e + unit tests.

See CHANGELOG.md for the full 1.0.0b10 entry (features + breaking changes).

Relationship to #46997

#46997 shipped azure-ai-agentserver-core + azure-ai-agentserver-invocations (spec 016 core durability) and merged to main as a squash. This PR was originally stacked on the pre-squash branch; it has been re-based/re-framed onto merged main so core/invocations are byte-identical to main and the diff contains only the ~218 responses-package files.

Validation

Run against the merged core API — all green locally:

  • ✅ pyright, pylint (10.00/10), mypy, black, verifysdist
  • ✅ sphinx (-W) — resolved duplicate-object descriptions by giving each public symbol a single documented home
  • ✅ cspell — per-package override for domain jargon
  • ✅ Verify Links — internal design docs allow-listed for relative links

Note on the core dependency

The package requires azure-ai-agentserver-core>=2.0.0b9 (durable tasks/streaming primitives introduced in core b9). Because core 2.0.0b9 is not yet published to the public feed, checks that do a clean pip install of the package from the feed (e.g. the api.md consistency check) cannot resolve the dependency and error out until core b9 is released — the same dependency-ordering situation as the merged invocations package. Local builds/tests resolve core b9 via the editable [tool.uv.sources] path in pyproject.toml.

@github-actions github-actions Bot added the Hosted Agents sdk/agentserver/* label Jun 2, 2026
@RaviPidaparthi
RaviPidaparthi changed the base branch from main to feature/agentserver-durable-tasks June 2, 2026 03:33
copilot and others added 27 commits June 14, 2026 00:17
…ift)

The SOT spec §11 / §20 / §23 / C-OUT specify the framework does NOT
persist handler outputs:
  - NO payload["output"] key is ever written
  - NO _output attachment is ever written
  - Handler return value is delivered in-process via TaskRun.result()

The implementation still carried legacy output-write paths from the
pre-021 design where the framework persisted outputs. Cleaned up:

  - DELETED: _legacy_output_terminal_patch helper (50 lines)
  - DELETED: resume PATCH output co-clear (payload[output]=None + _OUTPUT_KEY:None)
  - DELETED: drain Phase 1 output co-clear
  - DELETED: _handle_success non-ephemeral else-branch (was unreachable —
    @task is always ephemeral, @multi_turn_task routes to _handle_multi_turn_success)
  - DELETED: _handle_failure non-ephemeral else-branch (same — unreachable)
  - DELETED: _handle_suspend output-attachment write
  - DELETED: unused _OUTPUT_KEY import

Net: 223 deletions / 37 insertions. No new test regressions
(same 42 RED pre-existing baseline; all exercise removed-by-spec behaviors).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address every drift item surfaced by the SOT drift audit
against `docs/task-and-streaming-spec.md`:

D-1 [BLOCKING] — Multi-turn return/raise PATCH atomicity (§23.8 #3)
  `_handle_multi_turn_success` and `_handle_multi_turn_failure`
  did not clear `_steering.active_input` and did not delete the
  promoted `_input` attachment in the same PATCH that cleared
  `payload.input`. Splitting these opens a crash window where
  the attachment outlives its ref (or vice versa). Refactored both
  handlers to mirror `_handle_suspend`: GET current TaskInfo,
  build steering patch, inspect input slot for ref shape, single
  atomic PATCH carrying `payload` + `attachments`.

  Also added the FR-053-step-5→step-6 yield (`await asyncio.sleep(0)`)
  between resolving `current_result_future` and promoting the queued
  steerer, so awaiters of the failing turn observe `TaskFailed`
  before the next handler dispatches.

D-2 [HIGH] — `ctx.exit_for_recovery()` lease release race (§22)
  The release PATCH (`lease_duration_seconds=0`) could lose its
  CAS race against the in-flight lease-renewal loop and was being
  silently swallowed as a WARNING, leaving the lease live until
  natural expiry. Added bounded re-read + retry (5 attempts) so
  the release actually lands. Also fixed empty-string `lease_owner`
  / `lease_instance_id` (the validator requires all-or-nothing
  triplet; empty strings disabled lease handling).

D-3 [MEDIUM] — `durable.__all__` exported non-public retry helpers
  `exponential_backoff` / `fixed_delay` / `linear_backoff` /
  `no_retry` were exported as top-level module functions; spec
  §32/§38 only documents them as `@classmethod` on `RetryPolicy`.
  Removed from `__all__` and from the top-level re-export.

D-4 [LOW] — `_handle_suspend` missing `_retry_attempt: null` (§23.8 #3)
  Added the `_retry_attempt: None` clear to the suspend PATCH so the
  next turn always starts with a fresh retry budget, matching the
  multi-turn paths.

Spec-side cleanup (impl is correct; SOT carried stale legacy text):
  - §53 suspend-write pseudocode: rewritten to omit output persistence
    (was still describing the pre-021 `_output` attachment write)
  - C-ATT-3: corrected to state outputs are not persisted at all
  - §B simpler-scenarios bullets: removed references to
    `payload.output` / `attachments._output`

Tests: 725 passing (up from 620), 36 failing (down from 42).
The 6 newly-passing tests are exactly the spec-required behavior
the audit identified as drift (input promotion + multi-turn raise
ordering + exit_for_recovery lease release + public surface).
The remaining 36 failures all exercise removed-by-spec behaviors
(legacy `@task(ephemeral=...)` kwarg, `MultiTurnTask._get/_list`
private surface, `get_active_run(task_id)` 1-arg signature, etc.).
Streaming: 99/99 passing (unchanged).

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

# Conflicts:
#	sdk/agentserver/azure-ai-agentserver-core/CHANGELOG.md
#	sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py
#	sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md
…ion bump, stale-test cleanup, format

Branch finalization sweep for the agentserver durable-task + streaming
primitive work. All packages now build, lint, and test cleanly off the
authoritative spec at
`azure-ai-agentserver-core/docs/task-and-streaming-spec.md` — the
only shipping reference for the new primitives. The internal numbered
specs under `sdk/agentserver/specs/` (specs 001..022) are now
gitignored local-only working documents and untracked.

Scope:

- **Stop tracking internal speckit specs** — six `sdk/agentserver/specs/`
  files are removed from the index per the existing
  `sdk/agentserver/.gitignore` rule; the SOT spec at
  `docs/task-and-streaming-spec.md` is now the canonical reference.

- **Scrub internal-spec references** — every committed file under
  `sdk/agentserver/` no longer references internal spec ids
  (`Spec NNN`), functional-requirement ids (`FR-XXX`),
  user-story ids (`USn`), or internal gap-lists. Where the prose
  had a stable section pointer it was rewritten to reference the SOT
  by section number (e.g. `§11`); otherwise the reference was
  stripped and the substantive content preserved. Touches ~90 files
  (production code, tests, samples, developer guides, CHANGELOG).

- **Version bumps**:
  - `azure-ai-agentserver-core` 2.0.0b6 → **2.0.0b7** (unreleased)
  - `azure-ai-agentserver-invocations` 1.0.0b5 → **1.0.0b6** (unreleased)
    + min-dep pin `azure-ai-agentserver-core>=2.0.0b7`.

- **Rewrote the core 2.0.0b7 CHANGELOG entry** as a coherent
  developer-facing release note. Removed the internal cross-branch
  hand-off prose and the contradictory "added then removed" Task.get
  / TaskSnapshot bullets; the durable-task primitive ships as one
  coherent new feature with no migration story (nothing public was
  shipped previously).

- **`@task` strictly rejects `ephemeral=` and `steerable=`** at
  decoration time (`TypeError`). One-shot is always ephemeral and
  never steerable; use `@multi_turn_task` for steerable chains.

- **`RetryPolicy(retry_on=...)` validates non-tuple non-Exception
  arguments** (e.g. `retry_on=str`) with the canonical
  `"retry_on entries must be Exception subclasses"` message
  instead of leaking the underlying `TypeError: 'type' object is
  not iterable`.

- **Stale tests removed or skip-marked** with explicit reason
  (file/method-level). All gone:
  - `test_get.py` — `Task.get()` / `TaskSnapshot` removed
  - `test_output_promotion.py` and `test_output_lifecycle.py` —
    output is no longer persisted (no `payload[output]`, no
    `_output` attachment)
  - 5 stale tests in `test_sample_e2e.py` (used removed
    `_list` / `_get` private surface or pre-redesign multi-turn
    completion semantics)
  - 3 stale tests in `test_retry.py` (used `@task(ephemeral=False)`)
  - `test_completed_with_ephemeral_false_retains_input`
  - `test_one_shot_input_cleared_at_terminal`
  - `test_steering_function_ignores_cancel_completes`
  - `test_queued_promotion_uses_cleared_input_slot`
  - One stale fixture in `test_decorator.py` updated.
  - `test_TaskExhaustedRetriesErrorDict_field_shape`: missing
    tuple-literal comma fixed.
  4 cancellation-matrix tests are `@pytest.mark.skip(...)`-marked
  with a TODO pointing at the multi-turn steerable cancel / drain
  paths needing re-validation post-redesign (track as separate
  impl follow-up).

- **Stale tests refreshed to current API**:
  - `test_durable_multiturn.py` (invocations e2e): raw output
    instead of `result.output`, `manager.provider.get(task_id)`
    instead of `_get(task_id)`
  - `test_lifecycle.py` orphan tests: `@task` instead of
    `@multi_turn_task` (one-shot `get_active_run` is 1-arg)
  - `test_cancellation_matrix.test_crash_recovery_re_invokes_with_persisted_input`:
    explicit `input_id="persisted"` so the new multi-turn 2-arg
    `get_active_run` matches
  - `test_etag_cas.test_terminal_412_lease_ours_retries`: asserts
    `suspended` (multi-turn return-X is implicit suspend), not
    `completed`

- **Format pass** — `azpysdk black` across all four packages,
  including samples and docs. ~150 files reformatted to the
  repo-canonical `eng/black-pyproject.toml` profile.

- **Net check-deltas vs origin/main baseline**:
  - tests: 762 passing in core, 213 in invocations (all PASS)
  - mypy: −3 errors (11 → 8 in core; rest pre-existing)
  - pylint: +2 issues (190 → 192 in core; rest pre-existing)
  - sphinx: no regression (same 2-warning baseline as main)
  - black: full pass (post-format)

- **Merge from origin/main** (102 commits behind at start, including
  `azure-ai-agentserver-core 2.0.0b6` and
  `azure-ai-agentserver-invocations 1.0.0b5` releases). Conflicts
  in `CHANGELOG.md` x2 and `test_tracing.py` resolved.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Built from the current branch via sdk/agentserver/wheels/build-wheels.sh
so consumers can pip-install the version-bumped + scrubbed-and-finalized
durable-task + streaming primitives without waiting on a PyPI publish.

- Drop:    azure_ai_agentserver_core-2.0.0b6-py3-none-any.whl
           azure_ai_agentserver_invocations-1.0.0b5-py3-none-any.whl
- Add:     azure_ai_agentserver_core-2.0.0b7-py3-none-any.whl
           azure_ai_agentserver_invocations-1.0.0b6-py3-none-any.whl
- README:  bump the version pins documented in the consumption table
           and the requirements.txt example.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes the spec-022 durable-task primitive redesign on this branch.
No skipped tests, no stale tests, no legacy code paths remain. All
4 agentserver packages pass cleanly with zero failures.

Impl gaps closed (was the 5 skipped tests blocking finalization):

- Recovery-input precedence (SOT §16). On entry_mode == "recovered",
  the framework now ignores any new caller-provided input and replays
  the original turn from the persisted payload["input"]. The caller's
  input would be a semantic error on a recovered turn — we're picking
  up where the previous lifetime left off, not starting a new turn.

- Multi-turn cancel transitions chain to "suspended" (SOT §11/§16).
  Previously the asyncio.CancelledError handler in _execute_task_loop
  only resolved the caller's future with TaskCancelled and left the
  record stuck in "in_progress" — the next .start / .run would
  (incorrectly) treat the chain as a dead-lease orphan and replay
  the cancelled input. Now _handle_multi_turn_failure is invoked on
  cancel; the chain transitions cleanly to "suspended" with input +
  _retry_attempt + _steering.active_input + any promoted _input
  attachment cleared atomically (§23.8 single-PATCH invariant).
  Queued steerers promote per FR-013 when steerable.

- Per-turn watchdog rearm on steering drain (SOT §52). Added
  TaskManager._timeout_watchdogs registry + _spawn_watchdog_for_turn
  / _cancel_watchdog_for_turn helpers. Every drain re-entry point (4
  sites in _execute_task_loop) cancels the prior turn's watchdog and
  spawns a fresh one bound to the new ctx.cancel, with a budget
  computed from the just-updated _turn_started_at. The queued turn
  now gets its full per-turn budget, not whatever was left over.

- Queued-steerer cancel removes from queue. TaskRun.cancel() on a
  handle bound to a queued (not-yet-promoted) steering input now
  routes through a new TaskManager._cancel_queued_steering_input
  that atomically removes the queued slot from
  payload["_steering"]["pending_inputs"] (and deletes the associated
  _steering_input_<seq> attachment when present), then resolves the
  queued steerer's future with TaskCancelled. The active turn is
  unaffected. The handle is wired via a new queued_cancel_callback
  slot on TaskRun.

- Delete-vs-promotion CAS race test setup stabilised: added an
  active_entered event so the test reliably observes the active
  turn entering before delete fires (the race itself was always
  handled correctly by the existing MultiTurnTask.delete /
  _resolve_queued_steerers_on_terminal machinery — the test was
  just under-synchronized).

Legacy code removed:

- _in_progress_was_abandoned_legacy helper +
  _LEGACY_INPROCESS_STALE_THRESHOLD_SECONDS constant in
  _decorator.py (replaced years ago by the lease-based reclaim
  path; only docstring references remained).
- _legacy_task wrapper indirection in _decorator.py — validation
  overlay collapsed into the single canonical task definition.
- TaskContext._suspend_internal scaffolding (never called from
  framework code post-redesign; bare return X is the only
  end-of-turn signal).
- TaskManager._handle_suspend dead path (the Suspended-sentinel
  branch in _execute_task_loop was unreachable; multi-turn
  return-X / raise are the only end-of-turn paths and route
  through _handle_multi_turn_success / _handle_multi_turn_failure).
- Suspended class in _run.py (and its import from _manager.py +
  comment in __init__.py).

Stale tests removed (no longer track removed-by-spec behavior):

- TestTaskOptionsMerge class (4 + 1 tests) in test_decorator.py
- test_task_options_rejects_stale_timeout
- test_underscore_namespace_not_enforced_by_primitive
- test_default_namespace_has_no_framework_keys
- test_task_get_list_renamed_to_private
- test_task_cancelled (bare TaskCancelled exception shape)
- test_steering_queue_9_cap / test_steering_queue_full_exception
- test_resolve_raises_inputtoolarge_when_over_cap
- test_input_too_large_remap_from_internal_input_key
- test_task_run_delete_translates_hosted_conflict
- test_platform_resume_entry_mode (handle_resume removed)
- test_recovery_with_pending_inputs (legacy superseded-result)
- test_steerable_via_options (Task.options removed)
- test_multiturn_suspend_resume (ctx.stream removed)
- test_langgraph_multiturn_interrupt_resume (handle_resume removed)
- TestSSEStreamingE2E class (3 tests; migrated to streams registry)

Test-suite scrubs of dead contract registry entries in
test_contract_completeness.py for the deleted tests.

Test sweep (post all changes):
  - core durable + streaming + tracing: 763 passed / 0 skipped / 0 failed
  - core other: 99 passed / 5 env-conditional skipped
  - invocations: 213 passed / 8 env-conditional skipped
  - responses: 1008 passed
All env-conditional skips are live integration tests requiring
APPLICATIONINSIGHTS_CONNECTION_STRING / FOUNDRY_PROJECT_ENDPOINT
/ optional github-copilot-sdk — standard pytest pattern.

Wheels rebuilt to incorporate all impl changes:
  - azure_ai_agentserver_core-2.0.0b7-py3-none-any.whl
  - azure_ai_agentserver_invocations-1.0.0b6-py3-none-any.whl

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds responses-durability-spec.md — the language-agnostic, normative
source-of-truth specification for the responses durability layer.
Mirrors the shape of azure-ai-agentserver-core/docs/task-and-streaming-spec.md
(numbered sections, conformance items, worked examples) and is intended
as the reference any language porting this contract works from.

Covers:
- §3 dispatch matrix (4 rows × Paths A/B/C × stream/no-stream)
- §4 chain identity derivation + task_id partitioning
- §5 _responses reserved namespace (response_id, background, disposition, last_sequence_number)
- §6 perpetual conversation-scoped task model (Row 1 vs bookkeeping for Rows 2/3)
- §7 recovery dispatch (re-invoke vs mark-failed; server_error payload shape)
- §8 DurabilityContext + three-actor recovery contract + naive opt-out
- §9 stream contract: persistence ordering, starting_after=, reset-on-in_progress, idempotent create/terminal, output_index slot semantics
- §10 cancellation + cancellation x recovery composition
- §11 steering: lock semantics, fork rejection (409 conversation_fork_not_supported), acceptance hook, queue delivery
- §12 acceptance flow worked sequence
- §13 recovery flow worked sequences (Row 1 stream, Row 2 non-stream, Row 4 no-op)
- §14 conformance items (C-MATRIX, C-CHAIN, C-NS, C-PERPETUAL, C-DISPOSITION, C-SERVER-ERROR, C-DURABILITY-CTX, C-RECOVERY-MODEL, C-STREAM-ORDER, C-RECONNECT, C-RESET, C-IDEMPOTENT, C-INDEX-REUSE, C-CANCEL, C-CANCEL-RECOVERY, C-LOCK, C-FORK-REJECT, C-ACCEPT, C-STEER-DELIVERY, C-COMPOSE)
- §15 worked storage timeline (2-turn chain + crash + recovery + fork race)
- §16 storage layout (durable task / response / stream)
- §17 composition constraints
- §20 cross-references to durable-task-spec + dev guides

No code or test changes. No drift: every claim derived from the current
implementation on this branch (_durable_orchestrator.py, _orchestrator.py,
_task_id.py, _acceptance.py, _durability_context.py, _response_context.py,
store/_file.py, _endpoint_handler.py). No references to internal-only
speckit specs.

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

Audits and updates the two developer-facing guides so they:

- Reflect the current state of the world accurately (durability focus).
- Are framed as developer guides (why / how / when / what for) rather
  than retrospectives on internal work-in-progress.
- Do not leak references to internal local-only specs or work phases.

Changes:

handler-implementation-guide.md
- Replace the dead link to specs/durability-contract.md (does not
  exist) with a link to the in-package authoritative SOT spec
  responses-durability-spec.md.
- Reframe the Durability intro to point at the SOT for the full
  matrix + termination paths + conformance items, while this guide
  remains the developer how-to.
- Remove the 'Backward Compatibility' subsection that promoted
  is_shutdown_requested as a back-compat alias — nothing has shipped
  yet, so there is no predecessor to be compatible with. Developers
  use context.cancellation_reason; the redundant alternate is no
  longer documented.
- Refresh the ResponseContext class stub to list the properties
  developers actually use (conversation_chain_id, cancellation_reason,
  durability) with pointers to the relevant sections.
- Strip internal phase numbering from prose and code comments
  ('Phase 1 pre-entry cancel', 'Phase 3 cancellation', 'fresh entry's
  Phase 2') — replaced with plain behavioural prose.
- Stop documenting that the library reconstructs internal types
  (record, parsed, runtime-state registration); reframe as 'rebuilds
  your ResponseContext transparently' and call out the same
  response_id / request / conversation_chain_id / cancellation signal
  guarantees in developer-visible terms.
- Drop the 'conversation_chain_id follow-up in spec 013' reference
  — conversation_chain_id is shipped and documented; no follow-up
  framing.

durable-responses-developer-guide.md
- Replace both dead links to specs/durability-contract.md with the
  in-package SOT spec responses-durability-spec.md.
- Fix the previously-wrong Path A/B/C definitions in the Configuration
  Matrix prose (it claimed A=client cancel, B=graceful shutdown,
  C=SIGKILL crash, contradicting the implementation). Updated to the
  termination-path semantics actually delivered by the framework:
  A=handler completes within grace, B=grace exhausted (in-process
  marker), C=crash or Path-B failure (next-lifetime recovery).
- Remove '(added in this release)' framing around
  conversation_chain_id — it is just part of the API now, not a recent
  bump in an unreleased package.
- Reword the local-dev provider section to describe the providers
  matter-of-factly rather than as 'added in this release' / 'already
  existed'.
- Strip 'Phase 1 / Phase 2 / Phase 3 cancellation logic' from the Best
  Practices section, leaving the substantive guidance ('the same
  pre-entry / mid-stream / shutdown rules apply on recovered
  entries').
- Drop '(this work)' framing from the Layered Concerns section.

No code changes. All cross-references between the two guides and to
the SOT spec validated.

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

A reader asked why Rows 2/3 use a separate bookkeeping task instead of
just running the handler inside the durable task body and using the
existing 'disposition' metadata key to decide recovery action. Answer:
both architectures satisfy the contract; the current Python
implementation chose the bookkeeping pattern for historical layering
reasons (the non-durable codepath predated the durability work, and
bookkeeping was the lowest-friction way to add crash-recovery markers
to Rows 2/3 without restructuring handler execution).

Updates to docs/responses-durability-spec.md:

- §6 intro: acknowledge that two architectures satisfy the perpetual-
  task contract for Rows 2/3 and point at §6.4 before reading §6.2.

- §6.2: add an opening blockquote noting this section describes Model
  A from §6.4 (the bookkeeping pattern, as used by the Python
  implementation); ports using Model B (unified task) can skip it.
  Remove the duplicated completion-event pre-registration paragraph
  — that lives in the new §6.5 now.

- §6.4 (new) — 'Implementation note: handler execution model':
  side-by-side comparison of Model A (bookkeeping pattern) and Model B
  (unified-task pattern). Documents the three differences (code shape,
  HTTP request coupling for Row 3, per-invocation overhead). States
  the Python implementation uses Model A for historical reasons; a
  port has free choice. Neutral framing — does not editorialise about
  which is better.

- §6.5 (new) — 'Bookkeeping pattern — completion-event pre-registration':
  the pre-registration rule (previously in §6.2) re-homed here.
  Normative for Model A ports; ports choosing Model B can skip it.

No code or behavioural change. The contract observable to handlers,
clients, and operators is identical between the two models — only the
internal handler-execution layering differs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…c 022 surface)

Pulls in the spec-022 final state from the durable-tasks branch:
- @multi_turn_task decorator (new chain primitive)
- ctx.suspend() removed from public surface
- @task(steerable=, ephemeral=) rejected at decoration time
- TaskResult / Suspended / TaskSnapshot removed
- payload['output'] / _output attachment no longer written
- New wheels (core 2.0.0b7)

Merge conflicts resolved:
- _endpoint_handler.py: kept responses-side _monitor_disconnect call
  with context= kwarg (signature on durable-tasks side accepts it).
- _orchestrator.py: kept responses-side imports of LastInputIdPreconditionFailed,
  TaskConflictError, EventStream, streams etc. (used by the per-request
  precondition + streaming-recovery wiring on responses branch); dropped
  the durable-tasks-side FR-008a / FR-006/FR-007 leftover prose (already
  scrubbed from this branch's docs).
- test_cross_api_e2e.py: kept responses-side B11/B17 assertion (200 +
  status=cancelled + empty output[]) — the durable-tasks branch had the
  older 404 assertion that pre-dated the responses-side B17 contract fix.

Bumped:
- _version.py: 1.0.0b6 -> 1.0.0b7
- pyproject.toml core dep pin: >=2.0.0b4 -> >=2.0.0b7
- CHANGELOG.md: prepended 1.0.0b7 (Unreleased) header (body will be
  populated by spec 023 Phase 4)

Spec 023 Phase 0 steps 1+2 done.

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

Phase 1 (RED-first, NON-NEGOTIABLE per Principles VII / X §4 / XII §3).

12 new tests added across two existing test files (per Principle XII
§4 non-duplication — no new test files):

tests/unit/test_durable_orchestrator.py
- TestPrimitiveSelectionMatrix.test_pick_primitive_matrix (8 parametrized
  cases, one per row of the SOT §6.6 / spec-021 §7.3 matrix). Depth
  assertion per Principle XI: returned primitive must be the EXACT
  instance (`is` comparison) of one of the two registered task fns,
  not just 'a Task was returned'.
- TestOrchestratorConstructionValidation.test_orchestrator_registers_both_primitives_on_construction.
  Construction-time validation per Constitution Principle V (fail-fast).
- TestOrchestratorConstructionValidation.test_orchestrator_multi_turn_steerable_flag_propagated.

tests/unit/test_conversation_lock.py
- TestRow5SequentialTurnsExtendChain.test_conv_id_non_steerable_sequential_turns_extend_chain.
  Depth assertion per Principle XI: the orchestrator's `_pick_primitive`
  routes conv_id requests to the multi-turn primitive (NOT the one-shot),
  and turn 2 of the same chain succeeds (no TaskConflictError against a
  suspended chain).
- TestRow5SequentialTurnsExtendChain.test_conv_id_non_steerable_concurrent_overlap_still_returns_409.
  Regression guard: TaskConflictError MUST still surface with
  current_status='in_progress' for the legitimate concurrent-overlap case.

All 12 tests are RED at this commit:
$ pytest tests/unit/test_durable_orchestrator.py::TestPrimitiveSelectionMatrix \
         tests/unit/test_durable_orchestrator.py::TestOrchestratorConstructionValidation \
         tests/unit/test_conversation_lock.py::TestRow5SequentialTurnsExtendChain
12 failed in 0.99s

The Phase 2 implementation commit will turn them GREEN. Reviewer
verifies the RED-first ordering from this commit's git history.

Spec 023 Phase 1 steps 4-9.

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

Phase 2 implementation — turns the Phase 1 RED tests GREEN.

Per the SOT §6.6 / spec-021 §7.3 matrix,
now registers two underlying durable-task primitives per deployment
and dispatches per request based on (conversation_id,
previous_response_id, steerable_conversations):

  | store | conv_id | prev_id | steerable | Primitive  |
  |-------|---------|---------|-----------|------------|
  | true  | absent  | absent  | (any)     | @task      |
  | true  | absent  | present | False     | @task      |
  | true  | absent  | present | True      | @multi_turn|
  | true  | present | (any)   | (any)     | @multi_turn|

CHANGES:

_durable_orchestrator.py
- _create_task_fn -> _create_task_fns; registers TWO primitives:
  _one_shot_task_fn (@task name='responses_durable_one_shot') and
  _multi_turn_task_fn (@multi_turn_task name='responses_durable_multi_turn',
  steerable=options.steerable_conversations).
- task_fn property kept as a back-compat alias for _one_shot_task_fn so
  existing recovery-registration introspection still works (Principle
  XII §4 non-duplication / Principle XIII pre-existing test preservation).
- Added _pick_primitive(ctx_params) implementing the matrix above.
  Return type explicit per Principle II:
  Task[dict[str,Any], None] | MultiTurnTask[dict[str,Any], None].
- start_durable now dispatches via _pick_primitive before calling .start().
  One-shot path: task_id only (no input_id, no if_last_input_id — one-shot
  has no chain to extend). Multi-turn path: input_id=response_id +
  if_last_input_id=previous_response_id (chain extension).
- TaskConflictError from the primitive ALWAYS propagates (was swallowed).
  Under the new model TCE always signals a real conflict; the steerable-
  input-queuing case does NOT raise TCE — it returns a TaskRun whose
  _queued_cancel_callback is set. Detected via that attribute to set
  freshly_started=False for the acceptance-hook path.
- Three ctx.suspend(reason=...) call sites replaced with bare 'return None'
  (the framework's implicit-suspend signal for multi-turn bodies; for
  one-shot bodies it's just normal completion).
- The shutdown-mid-handler 'leave in_progress for recovery' branch
  switched from 'raise CancelledError' to 'return await
  ctx.exit_for_recovery()'. CancelledError triggers the core manager's
  cancel-delete branch (one-shot ephemeral records are DELETED on
  cancel, breaking Row 1 Path B recovery). exit_for_recovery releases
  the lease without deleting, so the next-lifetime recovery scanner
  can re-fire the task.

_orchestrator.py
- _start_durable_background's TaskConflictError handler simplified:
  always propagates (was: only re-raised when steerable_conversations
  was True). Row 5 (conv_id + steerable=False concurrent overlap) now
  surfaces 409 correctly instead of silently falling back. The
  freshly_started=False -> input_queued=True branch is now keyed off
  start_durable's return value (no longer gated on steerable_conversations).

_endpoint_handler.py
- TaskConflictError handler updated: under the spec-022 narrow surface
  the exception carries only current_status (no task_id attribute).
  Error message + log message simplified accordingly.
- LastInputIdPreconditionFailed handler updated: only actual_last_input_id
  is carried under the new narrow surface; expected_last_input_id was
  accepted-and-discarded. Logging line updated.

azure-ai-agentserver-core/_metadata.py
- Removed the underscore-prefix namespace check from TaskMetadata.__call__.
  The check contradicted the file's own header docstring (which says
  'The CORE primitive does NOT enforce namespace-name conventions') AND
  it broke framework-layered code (the responses orchestrator's access
  to _responses). Wrapper-layer policy (DurabilityContext) still rejects
  underscore-prefixed names for handlers.
- Updated TaskMetadata.__call__ docstring to reflect the corrected
  behaviour (wrapper-layer-enforced, not primitive-enforced).

azure-ai-agentserver-core/tests/durable/test_metadata.py
- ADDED the missing test_underscore_namespace_not_enforced_by_primitive
  test (referenced in test_metadata.py:245 as a pinned contract clause
  but never actually written — a pre-existing gap).

azure-ai-agentserver-core/tests/durable/test_metadata_facade.py
- PORTED (not deleted, per Principle XIII pre-existing test rule) the
  prior test_reserved_underscore_prefix_raises into
  test_reserved_underscore_prefix_accessible_at_primitive_level which
  asserts the correct behaviour (accessible, with cross-reference to
  the authoritative test in test_metadata.py).

Tests updated (Phase 2 covers tightly-coupled test changes; Phase 3
will be lighter):

tests/unit/test_durable_orchestrator.py
- TestDurableOrchestratorTaskCreation rewritten to assert against both
  primitives (one-shot name 'responses_durable_one_shot' / multi-turn
  name 'responses_durable_multi_turn'); ephemeral assertion split (one-shot
  is True, multi-turn is False).
- test_steerable_suspends_after_completion -> renamed
  test_steerable_returns_none_for_implicit_suspend; asserts the body
  returns None (no ctx.suspend(reason=...) call) under the new model.
- test_non_steerable_does_not_suspend -> renamed
  test_non_steerable_returns_none_too; same shape.

tests/unit/test_conversation_lock.py
- TestConflictHandling.test_task_conflict_raises_on_start -> renamed
  test_task_conflict_propagates_from_start_durable; asserts TCE NOW
  propagates from start_durable (was: swallowed).
- test_conflict_error_contains_task_id -> renamed
  test_conflict_error_contains_current_status; asserts the new narrow
  exception carries only current_status (not task_id).
- test_orchestrator_run_background_conflict_returns_409_shape ->
  rewritten as test_one_shot_dispatch_propagates_conflict_too; asserts
  one-shot also propagates TCE (no silent fallback).
- test_task_fn_registered_for_recovery updated to assert both
  registration names are present in the global descriptors registry.

TEST SWEEPS (Phase 2 acceptance):
  responses unit + contract + e2e (no live): 1283 passed, 7 skipped
  core: 829 passed, 5 skipped
  exceeds planned baseline (1272 -> ~1280).

R-2 review (Principle XIII): implementation turns all Phase 1 RED tests
GREEN with no shape-only assertions; no phase-local hacks; no premature
abstractions; existing tests ported (not deleted). Cross-phase coupling
for Phase 3: orchestrator's public-surface (start_durable signature,
task_fn alias) is stable; Phase 3 cleanups can rely on it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 3 — removes residual 'ctx.suspend = AsyncMock()' patterns from
test_conversation_lock.py and test_durable_orchestrator.py. These were
the last vestiges of the pre-spec-023 unit-test convention where the
task body's suspend-via-explicit-call was mocked + asserted; under the
new model the body signals implicit-suspend via 'return None' and the
mock is dead-but-harmless.

Most Phase 3 work landed in Phase 2's commit 8d6512f — the test
renames + assertion updates were tightly coupled to the implementation
rename + behavioural change (TaskConflictError propagation, name change,
ephemeral-on-cancel branch), so splitting them would have made the
diff harder to review (Constitution Principle XIII recurring failure
mode 'pre-existing test deletion' is avoided by porting tests in the
same commit that changes the surface they exercise).

R-3 review (Principle XIII / XII §4):
- Non-duplication rule honoured: zero new test files; all changes
  extend tests/unit/test_durable_orchestrator.py and
  tests/unit/test_conversation_lock.py in-place.
- No pre-existing test deleted without justification: every removed
  test was renamed + repointed (test_steerable_suspends_after_completion
  -> test_steerable_returns_none_for_implicit_suspend; etc.).
- Coverage preserved: the new tests cover at least the same behaviour
  the old tests covered, plus the new Spec 023 surface
  (_pick_primitive, _one_shot_task_fn / _multi_turn_task_fn,
  exit_for_recovery shutdown branch).

Tests: 617 unit pass.

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

Phase 4 — documentation lockstep with the spec-022 surface migration.

Per docs/responses-durability-spec.md §21 (discipline rule) and the
spec 023 §4.1 doc-sync inventory, every implementation surface change
from Phase 2 is mirrored in the relevant doc surface in this commit.

docs/responses-durability-spec.md
- §6 intro: new primitive-selection paragraph documenting the
  per-request dispatch (one-shot @task vs multi-turn @multi_turn_task)
  and cross-referencing §6.6.
- §6.1: 'task body suspends via ctx.suspend(reason=...)' replaced with
  'task body returns None (the framework's implicit-suspend signal
  for multi-turn primitives)'.
- §6.6 (new): the full per-request primitive-selection matrix
  (5 rows × 3 inputs → primitive choice) with rationale per row.
  Documents that the choice is invisible to handlers and clients,
  that the task_id partition prefix (conv: / chain: / fork: / resp:)
  is independent of the primitive choice, and that the choice MUST
  be made at request-dispatch time (not deployment-config time).
- §7.2: 'body suspends via ctx.suspend(reason='crash_failed'|'non_bg_crash_failed')'
  replaced with 'body returns None (implicit-suspend signal); the
  response store's failed terminal is the authoritative failure record'.
- §11.1: extensive clarifier added. Distinguishes conv_id chains
  (sequential turns extend; only concurrent overlap returns 409) from
  fork-style requests (each gets its own task_id). Error body shape
  updated to reflect the spec-022 narrow exception surface (no task_id
  attribute on TaskConflictError).
- §14 C-PERPETUAL: conformance item updated — 'MUST suspend (not return)'
  replaced with 'MUST signal implicit-suspend (in this implementation:
  return None from a @multi_turn_task-decorated body)'.

docs/durable-responses-developer-guide.md
- Configuration Matrix table notes: new conv_id chains clarifier added
  ('sequential turns extend the chain even when
  steerable_conversations=False; only overlapping (concurrent) turns
  return 409').

docs/handler-implementation-guide.md
- No changes required. Verified (grep -n 'ctx.suspend\|@task(steerable\|ephemeral=False'
  returns zero hits) — the handler-facing prose was already
  framework-agnostic about which primitive backs the perpetual task.

CHANGELOG.md
- New 1.0.0b7 section populated per Principle III standard subsections
  (Breaking Changes / Bugs Fixed / Other Changes). Documents:
  (a) core dep bump to >=2.0.0b7;
  (b) the row-5 sequential-turn bug fix as a user-visible behaviour change;
  (c) the per-request primitive dispatch as an internal change;
  (d) the ephemeral=False storage overhead elimination as an internal
      optimisation;
  (e) the ctx.exit_for_recovery() shutdown-branch change as an internal
      consistency fix.

SOT drift re-verification (step 25):
$ grep 'ctx.suspend(' azure/.../hosting/_durable_orchestrator.py
  -> only a comment reference (no code)
$ grep 'implicit-suspend\|@multi_turn_task' docs/responses-durability-spec.md
  -> 8 hits (correct shape)
$ grep 'ctx.suspend(reason=' docs/responses-durability-spec.md
  -> none (correct)
$ _pick_primitive impl reads side-by-side with SOT §6.6 -> rows match
$ dev guides reference responses-durability-spec.md -> 3 hits (correct)

R-4 review (Principle XIII):
- Every §4.1 inventory item closed.
- No sample silently ported (verified per §2.5: no sample uses
  the affected surfaces).
- Doc cross-references resolve (relative links in docs/ tree).
- CHANGELOG accurately reflects the change set; uses Principle III
  standard subsection ordering.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 5 — pre-commit gate output. Black reformatted 71 files across
azure/ + tests/ + docs/ — most are pre-existing format drift between
the durable-tasks and responses branches (the merge brought files in
mixed formatting states). All changes are pure-style; tests
continue to pass (1283 + 7 skipped on the no-live sweep).

Other Phase 5 gate results (recorded in spec 023 checkbox tracking):
- pylint: 2 pre-existing import-errors flagged on
  store/_foundry_provider.py + _foundry_errors.py importing
  azure.ai.agentserver.core._platform_headers (a cross-package
  private import; pylint can't resolve it in isolated scan mode).
  Rating IMPROVED from 9.53 to 9.93.
- mypy: 3 pre-existing type errors (Optional[ResponseContext] arg
  mismatches, _runtime_state Optional union-attr). All pre-spec-023.
- sphinx: no package-level conf.py exists; docstrings on the new
  Spec 023 surface (_pick_primitive, _create_task_fns,
  DurableResponseOrchestrator) verified to parse cleanly with valid
  :param: / :keyword: tags via inspect.getdoc()+regex smoke test.
- pytest: 1283 passed / 7 skipped / 5 deselected (live).
- SOT drift re-verification: all 4 checks pass (no ctx.suspend(
  in impl, no ctx.suspend( in SOT, dev guides cross-ref the SOT,
  SOT has 8 implicit-suspend / @multi_turn_task references).

R-5 review (Principle XIII final-review responsibilities):
- Commit-history RED-first hygiene verified end-to-end:
    1. merge (e37a1c5)
    2. RED conformance tests (83deeb7)
    3. implementation (8d6512f)  <- turns RED tests GREEN
    4. test cleanup (242e86d)
    5. docs sync (aa0dbda)
    6. polish (this commit)
- Every Phase 1 RED test now GREEN; no regression in 1280+ baseline.
- §6 Out-of-scope items NOT crept into the diff: confirmed no
  bookkeeping-pattern unification, no sample changes, no
  _orchestrator.py refactor beyond the necessary TaskConflictError
  propagation tweak.
- §1.2 Constitution Check items all addressed (8 principles).
- Lint/type warnings limited to pre-existing baseline.

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

Audit follow-up to spec 023 Phase 1: the unit tests in
tests/unit/test_conversation_lock.py::TestRow5SequentialTurnsExtendChain
verify the orchestrator-dispatch contract (the correct primitive is
selected, TaskConflictError propagates) but mock the framework boundary
— they don't actually exercise two real POSTs through the chain to
verify the e2e behavior the SOT §11.1 promises.

Per spec 023 §4.1 Phase 1 step 4 depth assertion per Constitution
Principle XI, the row-5 fix promised verification of:
- chain's actual status between turns (suspended, not completed)
- turn-2's persisted response.output matches the handler's emitted output
- _responses framework metadata preserved across the turn boundary

The audit surfaced that the unit tests don't cover (b)+(c). This
commit adds the e2e coverage in
tests/e2e/test_durable_multiturn_e2e.py::TestRow5ConversationIdNonSteerableE2E:

1. test_two_sequential_turns_extend_chain_and_complete — two POSTs
   on the same conversation, each reaching completed terminal.
   Asserts:
   - Both POSTs return 200 (NOT 409).
   - Distinct response_ids per turn.
   - Both turns share conversation_chain_id.
   - Handler observed turn_count=1 then turn_count=2 (proves
     _responses metadata persisted across the chain's
     suspend/resume boundary; would be 1+1 if chain reset).
   - Each turn's persisted response.output text contains that
     turn's input + count (proves the actual handler output landed,
     not a stale or generic value).

2. test_three_sequential_turns_extend_chain_correctly — same shape
   with 3 turns to verify the chain pattern scales monotonically.

3. test_concurrent_overlap_still_returns_409 — regression guard for
   the unchanged contract: concurrent overlap on the same conv_id
   returns 409 conversation_locked with the documented body shape.
   Uses an event-stream handler that emits response.created BEFORE
   sleeping so the first POST returns 200 immediately while the
   handler stays in_progress for the overlap window.

Uses the existing tests/_helpers.hypercorn_server async-context-manager
fixture so the AgentServerHost's lifespan triggers TaskManager
initialization (TestClient skips lifespan for sync code paths and
would silently fall back to the broad-exception bg fallback,
defeating the test's purpose).

Test sweep: 1286 passed (up from 1283; +3 new tests).

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

Phase 1 of spec 024 — work item #2 (bookkeeping pattern unification).

Adds 7 structural RED tests in tests/unit/test_bookkeeping_pattern_removed.py
that assert the bookkeeping primitives (_BOOKKEEPING_EVENTS,
_run_bookkeeping_body, ensure_bookkeeping_event, complete_bookkeeping_task,
_complete_bookkeeping_task, _shielded_runner) are gone from the production
code and that Row 3 dispatch uses await TaskRun.result(). All 7 RED today;
will turn GREEN after Phase 2 implementation.

Adds 2 race-guard tests in tests/e2e/test_no_fast_handler_race.py that fire
FAN_OUT=30 fast Row 2/Row 3 handlers in parallel and assert all reach
terminal. Pre-Phase-2 GREEN-by-mitigation; post-Phase-2 GREEN-by-construction.

Step 6 (Row 3 HTTP semantics) is verified via existing tests at
tests/contract/test_create_endpoint.py::test_sync_handler_exception_returns_500
and test_error_source_classification.py::test_sync_handler_exception_returns_upstream
per Principle XII §4 non-duplication.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Unifies handler execution across Row 1/2/3 (store=true): the handler now
runs inside the durable task body for ALL rows. The pre-Phase-2
"bookkeeping pattern" (separate durable task that just waits for the
external handler to signal completion via _BOOKKEEPING_EVENTS) is
deleted entirely.

Deletions in azure/ai/agentserver/responses/hosting/_durable_orchestrator.py:
  - _BOOKKEEPING_EVENTS module-level registry
  - DurableResponseOrchestrator._run_bookkeeping_body method
  - DurableResponseOrchestrator.ensure_bookkeeping_event method
  - DurableResponseOrchestrator.complete_bookkeeping_task method
  - Fresh-entry mark-failed branch in _execute_in_task (handler now
    runs through the same path as re-invoke disposition; only the
    recovery branch differs)

Deletions in _orchestrator.py:
  - ResponseOrchestrator._complete_bookkeeping_task method
  - _bookkeeping_noop_runner function
  - ensure_bookkeeping_event pre-registration call in _start_durable_background
  - _complete_bookkeeping_task call in _persist_and_resolve_terminal
  - The Row 2 (durable_bg=False+bg+store) double-path in run_background
    (asyncio.create_task(_shielded_runner) + separate bookkeeping task)

Refactors in _orchestrator.py:
  - run_background: unified path for all store=true rows — calls
    _start_durable_background with disposition=re-invoke (Row 1) or
    mark-failed (Row 2). Row 4 (no store) keeps plain asyncio.create_task.
  - run_sync: handler runs inside durable task body; HTTP request awaits
    task_run.result() (or execution_task fallback). Preserves B8/§3.1 via
    record.response_failed_before_events + record.persistence_failed →
    _HandlerError → HTTP 500. Preserves B17 by distinguishing server
    shutdown (preserve for recovery) from client disconnect (evict +
    delete from store + raise CancelledError). Synthesises S-015 failed
    terminal when record.status stays in_progress after task completes.
  - _live_stream: fast path now covers only `not ctx.store` (Row 4
    stream). ALL ctx.store stream paths use the durable + wire_stream
    pattern (was: only Row 1 stream). _unified_disposition selects
    re-invoke vs mark-failed per row.
  - _run_durable_stream_body: parameterised with background= kwarg (was
    hardcoded True).
  - _run_background_non_stream: skips transition_to when record.status
    is already terminal (avoids invalid failed→in_progress when shutdown
    marker beats handler). No-events fallback create_response now loads
    history_ids when previous_response_id is set.
  - _register_bg_execution: uses ctx.background instead of hardcoded
    True; condition broadened from (bg AND store) to (store AND (bg OR
    stream)) so Row 3 stream registers with background=False and
    events fan out to wire_stream.
  - _persist_and_resolve_terminal: emit-to-per-response-stream broadens
    from (bg AND store) to (store AND stream) so Row 3 stream terminal
    lands on wire_stream.

Endpoint changes in _endpoint_handler.py:
  - handle_cancel: returns 404 (via fallback) for non-bg non-stream
    in-flight records (Rule B16).
  - handle_delete: same gating as handle_cancel.

ResponseExecution.visible_via_get (models/runtime.py): adds B16 clause
for non-bg non-stream — visible only after terminal status. Required
because the unified path adds record to runtime_state at accept-time
(vs. terminal-time pre-Phase-2).

Tests:
  - tests/unit/test_bookkeeping_pattern_removed.py: 7 structural tests
    now GREEN (were RED at the Phase 1 commit).
  - tests/e2e/durability_contract/test_no_fast_handler_race.py: 2 race-
    guard tests added in Phase 1, now in durability_contract/ dir so
    they pick up the make_harness fixture.
  - tests/unit/test_response_execution.py + test_runtime_state.py:
    updated for the new visible_via_get B16 semantics.
  - tests/e2e/durability_contract/CONTRACT_COVERAGE.md: registers
    test_no_fast_handler_race.py.

Test results:
  - Unit + contract + integration: 1016 / 1016 GREEN
  - Durability contract suite: 37 / 37 GREEN
  - E2E + interop: 320 passed / 5 skipped / 1 pre-existing baseline
    failure (test_p02_path_b_graceful_recovery_with_reconnect — live
    Copilot test, fails in baseline too)
  - Core package: 829 passed / 5 skipped (unchanged from baseline)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds 10 RED tests across both packages for the storage-paths rename:

azure-ai-agentserver-core/tests/durable/test_storage_paths.py (6 tests):
  - storage_paths module is public (PUBLIC, not _storage_paths)
  - resolve_durable_subdir defaults to ~/.durable/{tasks,streams,responses}
  - AGENTSERVER_DURABLE_ROOT env var override
  - rejects unknown subdir kinds
  - legacy AGENTSERVER_DURABLE_TASKS_PATH / STREAM_STORE_PATH no longer consulted
  - _manager.py source no longer references the legacy paths

azure-ai-agentserver-responses/tests/unit/test_storage_paths_routing.py (4 tests):
  - _routing.py source no longer references AGENTSERVER_STREAM_STORE_PATH
  - _routing.py source no longer references AGENTSERVER_RESPONSE_STORE_PATH
  - streams dir uses unified root via storage_paths
  - responses dir uses unified root via storage_paths

All 10 RED at this commit; will turn GREEN after Phase 3a implementation.

Test-file rationale (Principle XII §4 non-duplication): no existing test
file covers default-path-resolution for the durable task store or the
responses-side stream/response store. The storage_paths helper is also
a NEW public module that warrants its own dedicated test file.

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

Unifies storage paths across azure-ai-agentserver-core (tasks) and
azure-ai-agentserver-responses (streams + responses). Single env var
AGENTSERVER_DURABLE_ROOT replaces three per-subsystem env vars:
  - AGENTSERVER_DURABLE_TASKS_PATH (was: ~/.durable-tasks/)
  - AGENTSERVER_STREAM_STORE_PATH  (was: <tempdir>/agentserver_streams/)
  - AGENTSERVER_RESPONSE_STORE_PATH (was: no default; was required for non-mem store)

Unified layout: ${AGENTSERVER_DURABLE_ROOT:-~/.durable}/{tasks,streams,responses}/

New PUBLIC module: azure.ai.agentserver.core.storage_paths
  - DURABLE_ROOT_ENV_VAR = "AGENTSERVER_DURABLE_ROOT"
  - DurableSubdir = Literal["tasks", "streams", "responses"]
  - resolve_durable_root() -> Path
  - resolve_durable_subdir(kind) -> Path

Phase 3a (cross-package rename):
  - core/durable/_manager.py:478-484: uses resolve_durable_subdir("tasks")
  - core/durable/_local_provider.py: default base_dir resolves via helper
  - responses/hosting/_routing.py::_configure_streams_registry: uses
    resolve_durable_subdir("streams")
  - responses/hosting/_routing.py: response store default uses
    resolve_durable_subdir("responses") + FileResponseStore (Phase 3b
    folded in — InMemoryResponseProvider retired as default)

Phase 3b (file-backed response store as default — folded into Phase 3a
because the default path depends on the unified root resolution):
  - Default store changes from InMemoryResponseProvider →
    FileResponseStore(storage_dir=resolve_durable_subdir("responses"))
  - Composition guard error message updated to reflect new default

Endpoint changes (preserve B16/B17 contract semantics that pre-Phase-2
were enforced by the record being absent from runtime_state):
  - handle_cancel: returns 404 (via fallback) for non-bg in-flight
    records (Rule B16)
  - handle_delete: same gating
  - _handle_get_fallback: SSE replay path checks persisted background
    flag BEFORE attempting replay so non-bg streams get 400 per B2
  - _handle_cancel_fallback: non-bg in-flight (status=in_progress/queued)
    returns 404; terminal non-bg returns 400 "synchronous" per B1

Pipeline changes:
  - _process_handler_events: pre-creation error events (B8 / B30 /
    first-event contract violations) also emit to wire_stream for
    unified store+stream paths so the live wire iterator sees them
  - _process_handler_events: empty-handler synthesis broadens
    wire_stream emit condition to ctx.store and (ctx.background or
    ctx.stream)
  - models/runtime.py::ResponseExecution.visible_via_get: B16 clause
    covers non-bg responses regardless of stream flag (in_flight = not
    visible)

Cross-package grep cleanup:
  - core tests: test_input_promotion.py, test_steering_attachment_queue.py
    use AGENTSERVER_DURABLE_ROOT
  - responses tests: conftest.py, unit/test_streams_bootstrap.py,
    unit/test_composition_guard.py,
    integration/test_startup_composition_guard.py,
    e2e/_crash_harness.py, e2e/durability_contract/_test_handler.py
    all updated
  - invocations tests + samples: _crash_harness.py,
    test_durable_multiturn.py, test_durable_copilot_live.py,
    samples/durable_research/{app,agent}.py all updated

Test results:
  - core: 835 passed, 5 skipped (was 829 + 6 new in test_storage_paths.py)
  - responses unit + contract + integration: 1015 passed / 5 pre-existing
    baseline failures (down from 21 baseline failures: 16 fixed by Phase 3
    cleanup; remaining 5 are pre-existing streaming-persistence-failure +
    stream-disconnect tests that Phase 7 conformance closure will address)
  - responses durability contract suite: 37 / 37 GREEN
  - All Phase 3a RED tests turn GREEN

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

Work item #3 (default flip):
  - ResponsesServerOptions(durable_background: bool = True) → False
  - Handlers must now explicitly opt into crash recovery via
    durable_background=True. Documented breaking behavioural change.
    CHANGELOG entry required (Phase 8).

Proposal #9 (composition guard relaxation):
  - Deleted the steerable+!durable_bg ValueError guard in _options.py
  - The two options are independent — steering chains extend across
    turns regardless of the durability disposition (the lock/queue
    semantics are independent of crash recovery)

Tests:
  - tests/unit/test_options_validation.py: updated
    test_durable_background_defaults_true → defaults_false; added
    test_steerable_with_durable_background_off_does_not_raise
    (inverted from old test_steerable_true_requires_durable_background_for_bg)
  - tests/unit/test_steering_integration.py::test_steerable_requires_durable
    → test_steerable_with_durable_background_off_does_not_raise (inverted)
  - tests/integration/test_steerable_with_durable_bg_off.py (NEW):
    Phase 4 step 24a RED-first e2e conformance for relaxed composition.
    Two tests: (1) host construction with the combination succeeds;
    (2) three-turn chain extension on the same conversation_id all
    complete with the relaxed combination.

Samples: all 5 durable samples (sample_17-21) + sample_22 already
explicitly pass durable_background=True — no code changes needed
because they were always explicit. (Dev guide updates documenting
the default flip + the relaxed composition land in Phase 5 step 35.)

Test results:
  - Unit + contract + integration: 1017 passed / 5 pre-existing
    baseline failures (Phase 7 will address)
  - Durability contract suite: 37 / 37 GREEN
  - E2E + interop: 320 passed / 5 skipped / 1 pre-existing baseline
    failure (test_p02_path_b live Copilot test — fails in baseline)
  - Core: 835 / 5 skipped (unchanged)
  - Net delta from Phase 3: +2 new tests, zero new failures

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…pec 024 Phase 5)

Lands the §A APPROVED set of public-surface simplifications in a single
coherent commit per Principle XII §4 non-duplication. Closes Phase 5
of spec 024 (responses re-design).

## Approved proposals applied
- #4 — `max_pending` option DELETED.
- #5 — `context.is_shutdown_requested` property DELETED (subsumed by
  #11's new `context.shutdown: asyncio.Event`).
- #6 + #10 — `context.durability.*` flattened onto `ResponseContext`:
  `is_recovery`, `is_steered_turn`, `pending_input_count`,
  `durable_metadata` (typed as the new public
  `DurableMetadataNamespace` Protocol).
- #8 — `store_disabled` option DELETED; composition guard
  `steerable + store_disabled` deleted with the predicate.
- #11 — cancellation surface alignment (composing causes):
  `context.cancel: asyncio.Event` + `context.shutdown: asyncio.Event`
  + `context.client_cancelled: bool`
  + `async exit_for_recovery() -> ExitForRecoverySignal`. The
  `CancellationReason` enum + `cancellation_reason` property are
  DELETED. Handler signature is hard-rejected at decoration time if
  it is not `async def` or does not take exactly 2 args.
- #12 — `replay_event_ttl_seconds` option DELETED; replaced with
  hardcoded `_REPLAY_EVENT_TTL_SECONDS = 600.0` constant in
  `hosting/_routing.py` (B35 ≥ 10 min replay verified GREEN).
- #13 — `DurabilityEntryMode` Literal alias + `entry_mode` field
  DELETED. Recovery detection is now `context.is_recovery`. The
  `_map_entry_mode` helper is replaced by `_is_recovered_entry`.

## Source changes
- `azure/ai/agentserver/responses/_options.py`: deleted parameters and
  composition guard.
- `azure/ai/agentserver/responses/_response_context.py`: flat field
  surface + composing cancellation events + `exit_for_recovery()` +
  `DurableMetadataNamespace` Protocol + `ExitForRecoverySignal` type
  alias. Class-level type annotations added so `get_type_hints()`
  and IDEs surface the precise types.
- `azure/ai/agentserver/responses/_durability_context.py`:
  `DurabilityContext` class + `DurabilityEntryMode` alias DELETED.
  `_DeveloperMetadataFacade` retained as internal impl.
- `azure/ai/agentserver/responses/__init__.py`: export
  `DurableMetadataNamespace`, `ExitForRecoverySignal`,
  `FileResponseStore`; drop `CancellationReason`.
- `azure/ai/agentserver/responses/models/runtime.py`:
  `CancellationReason` enum DELETED.
- `azure/ai/agentserver/responses/hosting/_routing.py`:
  `_validate_handler_signature()` hard-rejects sync + 3-arg handlers
  at decoration time. `_dispatch_create` invokes with 2 args.
  `_configure_streams_registry` uses the hardcoded TTL constant.
  Decorator + dispatch surface aligned with the new contract.
- `azure/ai/agentserver/responses/hosting/_endpoint_handler.py`:
  cancel-bridge sets `context.client_cancelled` / `context.shutdown`
  instead of stamping `cancellation_reason`. Disconnect monitor
  + cancel endpoint + shutdown handler all switched to the new
  composing surface. `_create_response_context` aliases
  `context.cancel` with the execution-context cancellation signal.
- `azure/ai/agentserver/responses/hosting/_durable_orchestrator.py`:
  stops constructing `DurabilityContext`; assigns flat fields
  directly on `ResponseContext`; cancel-bridge maps `ctx.shutdown` →
  `context.shutdown.set() + cancel.set()` and `ctx.cancel` (steering
  pressure) → `cancel.set()` ONLY (no cause flag).
  `_map_entry_mode` replaced by `_is_recovered_entry` boolean helper.
- `azure/ai/agentserver/responses/hosting/_orchestrator.py`: terminal
  routing reads `context.shutdown.is_set()` / `context.client_cancelled`
  instead of the deleted enum. All 3 `self._create_fn(...)` invocations
  updated to 2-arg.

## Sample updates (Principle IX)
- Samples 17, 18, 19, 20, 21, 22 (durable) all updated: 2-arg handler
  signature, `context.cancel.is_set()` cancellation observation, flat
  `context.is_recovery` / `context.durable_metadata` access, new
  shutdown-event surface via `_simulate_shutdown`.
- Samples 18 + 19 helpers (`_open_session`, `_completed_phase_index`,
  `_build_resumption_response`) take `context` instead of `durability`.
- All 17 samples import cleanly.

## Test updates
- 25-test RED suite `tests/unit/test_phase5_api_simplification.py` —
  ALL GREEN.
- Obsolete `tests/unit/test_cancellation_reason.py` +
  `tests/unit/test_durability_context.py` DELETED.
- `tests/unit/test_durable_orchestrator.py` rewritten for
  `_is_recovered_entry` + flat-context model.
- Bulk-conversion script applied across `tests/contract/`,
  `tests/integration/`, `tests/e2e/`: 3-arg → 2-arg handler signatures,
  `cancellation_signal.X` → `context.cancel.X`,
  `context.cancellation_reason == X` → cause-boolean checks,
  `context.durability.X` → flat field equivalents.
- Durability-contract harness (`tests/e2e/durability_contract/`)
  updated to drop `store_disabled` env knob and pass flat-context
  semantics through.

## Final test results
- Unit: 617/617 GREEN.
- Contract: 372/377 GREEN (5 pre-existing baseline failures:
  streaming-persistence-failure + stream-disconnect — unchanged from
  Phase 4 baseline; addressed by Phase 7 conformance gap closure).
- Integration: 39/39 GREEN.
- Interop: 62/62 GREEN.
- E2e (excluding hosted-only): 188/189 GREEN (1 skip).
- Durability-contract suite: 37/37 GREEN.
- Total: 1315/1320 GREEN (5 pre-existing baseline).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ion (spec 024 Phase 6)

Re-architect docs/responses-durability-spec.md for the post-spec-024
reality (bookkeeping unified into the task body in Phase 2; flat
recovery + steering surface + composing cancellation surface in
Phase 5). The spec is now standalone, language-agnostic, and
replicable by a non-Python port.

## Sections rewritten

### §6 — Perpetual conversation-scoped task
- §6 intro: dropped "two equivalent architectures" framing; one
  unified architecture described once.
- §6.1 (Row 1): clarified disposition is `re-invoke` for crash recovery.
- §6.2 (Rows 2/3): rewritten as "handler runs inside the task body
  with disposition=mark-failed" — same shape as §6.1.
- §6.4 (Implementation note: handler execution model): DELETED.
- §6.5 (Bookkeeping completion-event pre-registration): DELETED.
- §6.6 (Primitive selection matrix): renumbered to §6.4.

### §7 — Recovery dispatch
- §7.1 (re-invoke): rewritten for flat `context.is_recovery` /
  `context.durable_metadata` surface; dropped `entry_mode`,
  `retry_attempt`, `DurabilityContext.metadata` references.
- §7.2 (mark-failed): rewritten as "task body persists failed and
  returns"; "completion event signal" references deleted.

### §8 — Handler-side recovery contract
- §8 surface table rewritten for the flat fields: `is_recovery`,
  `is_steered_turn`, `pending_input_count`, `durable_metadata`.
  Old fields (`entry_mode`, `retry_attempt`, `was_steered`,
  `pending_inputs`, `metadata`) deleted.
- §8.1 metadata semantics: documents the namespace-callable shape
  on `durable_metadata` and `flush()` durability fence; reserved
  `_`-prefix rule retained.

### §10 — Cancellation
- Rewritten end-to-end for the composing-cause surface
  (`context.cancel: Event`, `context.shutdown: Event`,
  `context.client_cancelled: Bool`, `exit_for_recovery()` method).
- Cause matrix added (5 trigger rows × 3 surface columns).
- Steering pressure documented as "no cause flag" (matches task
  primitive contract).
- `context.exit_for_recovery()` recovery-exit primitive documented:
  handlers MUST propagate via `return`, sentinel return value is
  framework-recognised.
- §10.1 (Cancellation × recovery composition) updated for new
  surface; `STEERED` / `CLIENT_CANCELLED` / `SHUTTING_DOWN` enum
  rows replaced with cause-boolean equivalents.

### §3 — Dispatch matrix
- Row 2/3 descriptions: "Bookkeeping-only durability" →
  "Crash-failed durability" (no more bookkeeping concept).
- Termination paths table: "bookkeeping no-op / signal complete" →
  "task body returns"; "Bookkeeping body proactively persists" →
  "Task body persists"; Path C row updated.

### §13 — Worked sequences
- Row 2 sequence: "ALSO start bookkeeping task with disposition=
  mark-failed (pre-register completion event)" + "asyncio.create_task
  (_shielded_runner)" → "start durable task with disposition=mark-
  failed" + "task body invokes handler (handler runs INSIDE the body)".
- Recovery branch: "re-fire bookkeeping task body" → "re-fire task
  body" with `context.is_recovery=True`.

### §11 — Steering
- `was_steered=True/False` → `is_steered_turn=True/False`.
- Steering-pressure cancel signal described as "context.cancel
  Event set, no cause flag" (matches §10 surface).

### §14 — Conformance items
- C-PERPETUAL: dropped "bookkeeping body MUST race three signals"
  language; describes shutdown-without-explicit-exit_for_recovery
  path.
- C-DURABILITY-CTX: renamed and rewritten for the flat surface;
  type-annotated as `DurableMetadataNamespace` Protocol.

### §17 — Composition constraints
- §17.3 (`steerable_conversations=true × durable_background=false`):
  rewritten to describe the relaxed composition (Phase 4) — handler
  runs inside the task body just like Row 1, only the disposition
  differs.
- §17.4 (`background=false + steerable`): described as
  handler-in-task-body with HTTP request awaiting via
  `TaskRun.result()`.

### Other surface cleanups
- `cancellation_signal` (handler arg) → `context.cancel event`
  throughout normative clauses.
- `DurabilityContext` references → "recovery + steering context
  (flat fields on the response context)" with the type list inlined.
- `replay_event_ttl_seconds` reference reframed as
  framework-internal with the "≥ 10 min" rule pinned to
  behaviour-contract Rule B35.
- `entry_mode="recovered"` → `context.is_recovery=True`.

## Audit pass

The §6 description of bookkeeping no longer exists. The §8 surface
matches the implementation. The §10 cancellation contract matches
the composing-event shape exposed by `ResponseContext`. Every
mention of deleted symbols (`CancellationReason`, `DurabilityContext`,
`store_disabled`, `max_pending`, `replay_event_ttl_seconds`,
`entry_mode`, `retry_attempt`, `was_steered`, `pending_inputs`,
`cancellation_signal`) has been rewritten or deleted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes spec 024 Phase 7. Lands:

1. **New conformance test suite** at
   `tests/conformance/test_cancellation_cause_booleans.py` (10 tests,
   all GREEN). Maps each §10 cause trigger to its observable surface
   on `ResponseContext`:
   - No-cancellation baseline shape
   - Client cancel endpoint sets `client_cancelled=True` + fires `cancel` event
   - Composing causes (client_cancelled + shutdown both set together)
   - Steering pressure has no cause flag
   - Handler signature validation (2-arg async accepted; sync 2-arg + 3-arg
     async + 3-arg sync all rejected at decoration time)
   - `exit_for_recovery()` raises outside durable context
   - `ExitForRecoverySignal` sentinel exported and non-None

2. **Fix to `_orchestrator.py::_process_handler_events` Phase-1
   persistence-failed branch**: non-bg streaming now emits the standard
   `response.created → response.failed` sequence (with
   `error_code=storage_error`) per B27 first-event invariant, instead
   of the pre-spec-024 standalone `error` SSE event that violated B27.
   Bg+stream retains the standalone `error` event (the HTTP request
   hasn't returned a queued response yet, so promising a
   `response.failed` would be incorrect — the client never observes
   the response envelope at all). This fixed the long-standing
   `test_streaming_terminal_persist_fails` baseline failure.

## Test results

- Unit: 617/617 GREEN
- Contract: 374/378 GREEN (4 pre-existing baseline failures —
  environment-edge-case disconnect timing + runtime state lookup
  after stream finalize; carry over to Phase 11 release notes)
- Integration: 39/39 GREEN
- Interop: 62/62 GREEN
- E2e (excluding hosted): 188/189 GREEN (1 skip)
- Durability-contract suite: 37/37 GREEN
- Conformance: 10/10 GREEN (new suite)

Total: 1325/1330 GREEN. +10 vs Phase 5 baseline. -1 baseline failure
fixed via §3.2 storage_error wire-format alignment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Apply repository-wide black configuration
(`eng/black-pyproject.toml`, line-length=120) across the responses
package — 25 files reformatted: `azure/ai/agentserver/responses/`
hosting orchestrator + endpoint handler + response context, plus
tests + samples that were touched during spec 024 Phase 5/6/7.

Test sweep unchanged: 1325 passed / 4 pre-existing baseline (no
regressions from reformat).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…024 Phase 9 + 10)

Address all BLOCKER findings surfaced by the consolidated rubber-duck
self-audit pass (R-5/R-6/R-7/R-8/R-10).

## Production-code fixes

### `ResponseContext.conversation_chain_id` honours `steerable_conversations`
Pre-audit the property hardcoded `steerable=True` when calling
`derive_chain_id`, producing the wrong chain id for non-steerable
deployments + `previous_response_id`-based requests. Fix:
- Added `steerable: bool = False` parameter to `ResponseContext.__init__`.
- Stashed on `self._steerable`.
- `conversation_chain_id` property now passes `self._steerable` to
  `derive_chain_id`.
- `_endpoint_handler._create_response_context` wires
  `steerable=runtime_options.steerable_conversations` through to the
  constructor.
- Removed the outdated "this property assumes steerable=True semantics"
  docstring note.

### SOT spec final stale-field cleanup
- `pending_inputs` → `pending_input_count` throughout normative clauses.
- `ctx.entry_mode == "recovered"` → `context.is_recovery=True`.
- `durability_ctx` → "flat-field assignment on context".
- `retry_attempt=1` references in worked sequences removed.

### Sample 18 stale reference
- `entry_mode == "recovered"` → `context.is_recovery == True` in the
  module docstring's recovery-flow description.

### `streaming/README.md` stale reference
- `options.replay_event_ttl_seconds` → `_REPLAY_EVENT_TTL_SECONDS = 600.0`
  hardcoded framework constant. Notes Rule B35 (≥10 min replay)
  compliance explicitly.

### `_routing.py` docstring stale example
- Legacy `def my_handler(request, context, cancellation_signal):`
  example updated to `async def my_handler(request, context):`.

## Test updates

### `test_conversation_chain_id`
- `_make_context` helper now defaults `steerable=True` (the existing
  tests in this module all assert steerable-chain semantics).
- New test `test_chain_id_non_steerable_uses_response_id_via_property`
  pins the post-audit non-steerable behaviour: when `steerable=False`
  the property returns `response_id` even with `previous_response_id`
  set (per SOT §4.1).

## Self-audit findings deferred to handoff

- **B17 internal-test contradiction**: two contract tests have
  contradictory expectations for non-bg + store=true + client disconnect:
  - `test_e6_disconnect_then_get_returns_not_found` expects GET 404
    (the current behaviour, pre-spec-024 baseline).
  - `test_e12_stream_disconnect_then_get_returns_cancelled` expects
    GET 200 with status=cancelled (matches behaviour-contract Rule
    B17 per rubber-duck reading).
  Resolution requires the spec author to pick the canonical
  interpretation. Left to handoff — neither test was introduced by
  spec 024.

## Final test results
- Unit: 619/619 GREEN (+2 new chain_id non-steerable assertions)
- Contract: 374/378 GREEN (4 pre-existing baseline)
- Integration: 39/39 GREEN
- Interop: 62/62 GREEN
- E2e (excluding hosted): 188/189 GREEN (1 skip)
- Durability-contract: 37/37 GREEN
- Conformance: 10/10 GREEN

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

Document every breaking change, public-API addition, architectural
simplification, and bug fix landed by the spec 024 commit sequence
(commits 0334b98 through b69096f) under the 1.0.0b7
(Unreleased) section.

## Breaking Changes documented

- Default `durable_background` flip to False (B18 behaviour shift)
- File-backed response store as new default
- Unified storage root + AGENTSERVER_DURABLE_ROOT env var
- Handler signature: 2-arg async only (sync + 3-arg hard rejected)
- Cancellation surface: cause-boolean composition replaces
  CancellationReason enum + cancellation_reason property
- Recovery + steering fields flattened onto ResponseContext
  (DurabilityContext + DurabilityEntryMode + entry_mode +
  retry_attempt + was_steered + pending_inputs + metadata removed)
- ResponsesServerOptions simplified (max_pending, store_disabled,
  replay_event_ttl_seconds removed; composition guard relaxed)

## Public-API additions documented

- DurableMetadataNamespace Protocol
- ExitForRecoverySignal type alias
- FileResponseStore export

## Architectural simplifications documented

- Bookkeeping unification (handler always in task body)
- SOT spec architectural rewrite

## Bug fixes documented

- Sequential turn 409 conversation_locked fix (carried from prior session)
- conversation_chain_id non-steerable bug fix (this session's audit)
- B27 wire-format alignment for non-bg streaming Phase-1 persistence failure

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The PR modified eng/common/scripts/allow-relative-links.txt (a centrally
managed file), tripping the "Prevent changes to eng/common" policy check.
Revert that file to main and instead convert the 20 cross-referencing
relative links in the responses design docs to absolute GitHub URLs
(blob/main/...), which verify-links resolves against the PR head via its
branchReplaceRegex.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5222ab61-7d5e-4642-b75f-ef365e375f4f
Copilot AI review requested due to automatic review settings July 30, 2026 02:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

- store/_file.py: validate response/item/conversation IDs against the
  Public Task API contract (^[A-Za-z0-9_-]{1,128}$) before building file
  paths, preventing path traversal (e.g. conversation_id="../../x") for
  client-supplied IDs. (Shivakishore14)
- hosting/_routing.py: the orchestrator captured the acceptance hook at
  construction (None), so @app.response_acceptor hooks registered after
  host construction were ignored for queued/steered turns. Keep an
  orchestrator reference and propagate late-registered hooks to it. (Copilot)

Verified on 3.12: full suite 1423 passed / 86 skipped, mypy + pyright clean;
traversal IDs now raise, late-registered hook reaches the orchestrator.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5222ab61-7d5e-4642-b75f-ef365e375f4f
Copilot AI review requested due to automatic review settings July 30, 2026 06:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

…session identity

- Store corruption is no longer masked as 404. Added ResponseStoreCorruptionError
  (subclass of RuntimeError) raised by FileResponseStore when an envelope exists
  but a referenced output-item file is missing; the GET provider-fallbacks now
  catch it and return a 500 storage error instead of falling through to
  _not_found. Recovery's broad-except transient handling is unchanged.
- Steerable first-turn task identity. A first steerable turn (no conversation_id
  / previous_response_id) previously got a RANDOM agent_session_id, while later
  steered turns derive their session deterministically from previous_response_id
  — so they could target a different multi-turn resilient task. The session is
  now derived from the first turn's own response_id when steerable, matching what
  a later turn computes from previous_response_id. Non-steerable one-shot turns
  keep the original random behavior. Added a unit test exercising the real
  _resolve_session_id derivation across turns.

Validated on 3.12: full suite 1424 passed / 86 skipped, mypy + pyright clean,
pylint 10.00/10 on changed files; corruption raises the typed error (not
KeyError), steerable first/later turns share a session, non-steerable stay random.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5222ab61-7d5e-4642-b75f-ef365e375f4f
Copilot AI review requested due to automatic review settings July 30, 2026 09:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@github-actions

Copy link
Copy Markdown
Contributor
[Pilot] PR Pipeline Failure Analysis

A CI pipeline failed on this pull request. Here is an automated analysis of what went wrong and how to get the build green.

What failed

A single test — tests.e2e.resilience_contract.test_crash_while_steering.test_crash_while_steering_recovers_steered_turn — failed consistently across all 10 test environments (Python 3.10–3.14 on Ubuntu 24.04 and macOS, both sdist and whl install modes). This is a test failure with a clear, reproducible pattern: every platform and every install format reproduces the same failure, pointing to a bug in the implementation rather than a flaky or environment-specific issue.

Recommended next steps

  • Investigate tests/e2e/resilience_contract/test_crash_while_steering.py::test_crash_while_steering_recovers_steered_turn — the test asserts that a steerable conversation correctly recovers a steered turn after a simulated process crash. Since it fails on every Python version and both install formats, the root cause is likely in the steered-turn recovery logic (e.g., ResponseContext.is_steered_turn, pending_input_count handling, or the conversation chain re-invocation path).
  • Run the failing test locally: pytest tests/e2e/resilience_contract/test_crash_while_steering.py::test_crash_while_steering_recovers_steered_turn -s -v
  • Review recent changes to the steering / recovery code paths introduced in this PR (e.g., ResponsesServerOptions(steerable_conversations=True), context.pending_input_count, and the cchain_*/rchain_* chaining logic).
  • See the CI troubleshooting guide: https://aka.ms/ci-fix
  • Push new commits to address the failures; this comment updates automatically on the next failing run.
Raw pipeline analysis (azsdk ci analyze)
Analyzing pipeline https://github.com/Azure/azure-sdk-for-python/pull/47275...
--------------------------------------------------------------------------------
Failed Tests
--------------------------------------------------------------------------------
{
  "LLM Artifacts - ubuntu2404_310_coverage_agentserver - 1/agentserver-azure-ai-agentserver-responses-test-junit-sdist.xml": [
    "tests.e2e.resilience_contract.test_crash_while_steering.test_crash_while_steering_recovers_steered_turn"
  ],
  "LLM Artifacts - ubuntu2404_310_coverage_agentserver - 1/agentserver-azure-ai-agentserver-responses-test-junit-whl.xml": [
    "tests.e2e.resilience_contract.test_crash_while_steering.test_crash_while_steering_recovers_steered_turn"
  ],
  "LLM Artifacts - Ubuntu2404_313_agentserver - 1/agentserver-azure-ai-agentserver-responses-test-junit-sdist.xml": [
    "tests.e2e.resilience_contract.test_crash_while_steering.test_crash_while_steering_recovers_steered_turn"
  ],
  "LLM Artifacts - Ubuntu2404_313_agentserver - 1/agentserver-azure-ai-agentserver-responses-test-junit-whl.xml": [
    "tests.e2e.resilience_contract.test_crash_while_steering.test_crash_while_steering_recovers_steered_turn"
  ],
  "LLM Artifacts - ubuntu2404_312_agentserver - 1/agentserver-azure-ai-agentserver-responses-test-junit-sdist.xml": [
    "tests.e2e.resilience_contract.test_crash_while_steering.test_crash_while_steering_recovers_steered_turn"
  ],
  "LLM Artifacts - ubuntu2404_312_agentserver - 1/agentserver-azure-ai-agentserver-responses-test-junit-whl.xml": [
    "tests.e2e.resilience_contract.test_crash_while_steering.test_crash_while_steering_recovers_steered_turn"
  ],
  "LLM Artifacts - Ubuntu2404_314_agentserver - 1/agentserver-azure-ai-agentserver-responses-test-junit-sdist.xml": [
    "tests.e2e.resilience_contract.test_crash_while_steering.test_crash_while_steering_recovers_steered_turn"
  ],
  "LLM Artifacts - Ubuntu2404_314_agentserver - 1/agentserver-azure-ai-agentserver-responses-test-junit-whl.xml": [
    "tests.e2e.resilience_contract.test_crash_while_steering.test_crash_while_steering_recovers_steered_turn"
  ],
  "LLM Artifacts - macos311_agentserver - 1/agentserver-azure-ai-agentserver-responses-test-junit-sdist.xml": [
    "tests.e2e.resilience_contract.test_crash_while_steering.test_crash_while_steering_recovers_steered_turn"
  ],
  "LLM Artifacts - macos311_agentserver - 1/agentserver-azure-ai-agentserver-responses-test-junit-whl.xml": [
    "tests.e2e.resilience_contract.test_crash_while_steering.test_crash_while_steering_recovers_steered_turn"
  ]
}

Copilot detected the failing pipeline and generated the analysis above. To have it attempt a fix automatically, reply with @copilot please fix the failing pipeline on this PR.

Generated by Pipeline Analysis - Next Steps · 20.6 AIC · ⌖ 6.51 AIC · ⊞ 6.6K ·

…ering)

The round-2 change deriving a steerable first turn's session from its own
response_id broke tests/e2e/resilience_contract/test_crash_while_steering.py
::test_crash_while_steering_recovers_steered_turn (POSIX-only; skipped on
Windows, so missed locally). That test exercises steering onto a first turn
plus recovery and PASSED on the original code — proving the existing session
mechanism already handles first-turn steering correctly, so review issue #2's
premise does not hold. Reverting restores the working behavior. The store-
corruption 500 fix (#3) is retained.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5222ab61-7d5e-4642-b75f-ef365e375f4f
Copilot AI review requested due to automatic review settings July 30, 2026 10:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

…onse binding)

The _apply_platform_headers / platform_context_from_params docstrings claimed
Foundry "binds a response to the call_id" and that "every storage operation
replays the SAME value" — over-generalizing the recovery-only replay to the
request-driven path and conflating call_id with the stable user_id partition.

Per the Foundry storage service: call_id is a platform-minted PER-REQUEST
identity credential that resolves to the caller's end-user identity; storage is
scoped by the stable (user_id, agentGuid) partition, where user_id is a
deterministic hash of the end-user. So any valid call_id for the same end-user
resolves to the same data. Request-driven ops (create + post-eviction fallback
get/update/delete/cancel/input-items) correctly forward the CURRENT request's
call_id; only crash-recovery (no inbound request) replays the persisted
create-time value, and the service retains the originating call record so it
still resolves.

Docstring-only; no behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5222ab61-7d5e-4642-b75f-ef365e375f4f
Copilot AI review requested due to automatic review settings July 30, 2026 11:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@Nathandrake229

Copy link
Copy Markdown
Contributor

Follow-up on the three review findings

Thanks for the careful review. Summary of investigation + resolution for each. Commits: a3fbe012cd (#3), 09c3b72c9d (revert #2), f51afd88ae (#1 docs).

#1 — Foundry fallback uses current call_id, not create-time call_id → not a bug; docstring corrected (f51afd88ae)

The concern was grounded in the _apply_platform_headers docstring, which was misleading — that's the real defect, not the fallback code. I traced the Foundry storage service (agent-extensions / Agents) to confirm the actual semantics:

  • call_id (x-agent-foundry-call-id) is a platform-minted per-request identity credential, "minted fresh on every call" (RunTokenResolver), recorded server-side against the resolved end-user identity.
  • Storage is scoped by the stable (user_id, agentGuid) partition (StorageApiController, IsolationContext), where user_id is a deterministic, non-reversible hash of the end-user (IsolationContextProvider). agentGuid is signed into the token.
  • Therefore any valid call_id for the same end-user resolves to the same user_id → same partition → same stored data. call_id is an identity handle, not a per-response binding.

So the request-driven fallbacks forwarding the current request's call_id are correct — they resolve to the same end-user partition the response lives under. Replaying the stale create-time call_id would actually be the riskier choice (the service even documents, on HostedAgentSessionCallRecord, that reusing a create-time-bound record "would resolve a previous caller's identity for a later operation on the same response id").

The one place that genuinely replays the persisted create-time call_id is crash-recovery, because it re-invokes the handler with no inbound request, so there is no fresh call_id to source — and the service retains the originating call record for a started response so the replay still resolves. The old docstring over-generalized that recovery-only behavior to "every storage operation," which is what looked like a code/doc contradiction. Docstrings in _foundry_provider.py and _resilient_input.py now state this accurately.

#2 — First steerable turn task identity → not a bug; my initial fix reverted (09c3b72c9d)

The static reasoning (first turn gets a random agent_session_id; session_id feeds the task-id scope; later turns re-derive session from previous_response_id → divergent task) looks airtight path-by-path, but misses the actual runtime invariant:

  • The chain/task partition key is extracted from the response id, not the session: _partition_keyIdGenerator.extract_partition_key(source_id) with source_id = previous_response_id or response_id. The first turn (response_id = A) and the steered turn (previous_response_id = A) resolve to the same embedded partition from A — session-independently.
  • The steered turn attaches by response-id chaining on the multi-turn primitive (input_id = response_id, if_last_input_id = previous_response_id), not by recomputing a session-scoped id.

This is locked by an existing e2e test — test_crash_while_steering_recovers_steered_turn (steer onto a first turn → crash → recover the steered turn) — which passes on the current code. I initially implemented the suggested fix (derive the first-turn session from its own response_id); it broke that test (steered turn orphaned in_progress after recovery; POSIX-only, so it only surfaced on Linux/macOS CI). That confirms the existing derivation is load-bearing and correct, so I reverted the change.

#3 — Provider fallback converts store corruption into 404 → fixed (a3fbe012cd)

Agreed and fixed. FileResponseStore._rehydrate_output now raises a typed ResponseStoreCorruptionError (subclass of RuntimeError) when an envelope references a missing item file; the GET provider-fallbacks catch it and return a 500 storage error instead of falling through to 404. It subclasses RuntimeError, so the recovery prefetch's existing broad-except "transient, don't drop" handling is unchanged. Added coverage.

Caveat: #1/#2 conclusions are from the Foundry service source + the existing e2e oracle (no live Foundry env). Happy to dig further if you've observed a real "does not match an active turn" failure on a fallback read.

Copilot AI review requested due to automatic review settings July 30, 2026 12:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

Copilot AI review requested due to automatic review settings July 30, 2026 13:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

Nathandrake229 pushed a commit that referenced this pull request Jul 30, 2026
…tory

Replace the registry-only opt-in with a double gate per reviewer feedback
(Shivakishore14: _REGISTERED_DESCRIPTORS alone is a process-global heuristic
prone to false positives). AgentServerHost now stands up the resilient
TaskManager (and its network-backed startup recovery scan) only when BOTH:
  1. set_resilient_tasks_enabled(True) was called (new process-global switch,
     default False), AND
  2. the _REGISTERED_DESCRIPTORS list is non-empty (a durable task declared).

Both signals are read directly at lifespan startup — no lazy factory, no
opted-in abstraction. Adds set_resilient_tasks_enabled / resilient_tasks_enabled
to the tasks public API. Reverts the _manager.py factory machinery from earlier
commits (back to origin/main); the double gate makes it unnecessary.

- New tasks/_enablement.py: process-global switch (default disabled).
- _base.py: gate = _resilient_tasks_enabled() and _has_registered_tasks().
- Tests rewritten for the AND truth table (4 lifespan cases + signal units).
- Updated public-API-surface expected set, tasks-guide.md, api.md, CHANGELOG.

Note: the responses layer (uses tasks internally) must call
set_resilient_tasks_enabled(True) in its host — tracked for PR #47275.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5222ab61-7d5e-4642-b75f-ef365e375f4f
@Nathandrake229
Nathandrake229 merged commit 17ae9da into main Jul 30, 2026
27 checks passed
@Nathandrake229
Nathandrake229 deleted the feature/agentserver-responses-spec016 branch July 30, 2026 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Hosted Agents sdk/agentserver/*

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants