feat(agentserver-responses): resilient/durable responses orchestration (1.0.0b10) - #47275
Conversation
…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
- 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
…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
[Pilot] PR Pipeline Failure AnalysisA CI pipeline failed on this pull request. Here is an automated analysis of what went wrong and how to get the build green. What failedA single test — Recommended next steps
Raw pipeline analysis (azsdk ci analyze)
|
…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
…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
Follow-up on the three review findingsThanks for the careful review. Summary of investigation + resolution for each. Commits: #1 — Foundry fallback uses current call_id, not create-time call_id → not a bug; docstring corrected (
|
…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
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 tomain). This branch has been re-framed on top of mergedmain, so the diff is scoped to the responses package only.What's included
ResponsesServerOptions(resilient_background=True)makesstore=true,background=trueresponses 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.ResponsesServerOptions(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.ResponseContextresilience + steering surface — flat per-invocation fields:is_recovery,is_steered_turn,pending_input_count, andconversation_chain_id(nativecchain_*/rchain_*id convention;task_id == conversation_chain_id).yield stream.checkpoint()persists a response snapshot at a developer-chosen boundary;context.persisted_responseseeds 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 publicConversationChainMetadataNamespaceprotocol.await context.exit_for_recovery()— uniform graceful-shutdown recovery primitive across all handler shapes.GET /responses/{id}?stream=true&starting_after=<event_id>.@app.response_acceptorcustomizes the response shape returned when a turn is queued behind an active steerable conversation.FileResponseStore(default local-development store) exported fromazure.ai.agentserver.responses; typedResponseAlreadyExistsErrorfor idempotent-create on recovery.See
CHANGELOG.mdfor the full1.0.0b10entry (features + breaking changes).Relationship to #46997
#46997 shipped
azure-ai-agentserver-core+azure-ai-agentserver-invocations(spec 016 core durability) and merged tomainas a squash. This PR was originally stacked on the pre-squash branch; it has been re-based/re-framed onto mergedmainsocore/invocationsare byte-identical tomainand the diff contains only the ~218 responses-package files.Validation
Run against the merged core API — all green locally:
-W) — resolved duplicate-object descriptions by giving each public symbol a single documented homeNote on the core dependency
The package requires
azure-ai-agentserver-core>=2.0.0b9(durabletasks/streamingprimitives introduced in core b9). Because core2.0.0b9is not yet published to the public feed, checks that do a cleanpip installof the package from the feed (e.g. the api.mdconsistencycheck) 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 inpyproject.toml.