Skip to content

feat(agentserver): resilient task primitive + event streaming (core 2.0.0b7, invocations 1.0.0b6) - #46997

Merged
Nathandrake229 merged 317 commits into
mainfrom
feature/agentserver-durable-tasks
Jul 28, 2026
Merged

feat(agentserver): resilient task primitive + event streaming (core 2.0.0b7, invocations 1.0.0b6)#46997
Nathandrake229 merged 317 commits into
mainfrom
feature/agentserver-durable-tasks

Conversation

@RaviPidaparthi

@RaviPidaparthi RaviPidaparthi commented May 19, 2026

Copy link
Copy Markdown
Member

Summary

Adds a resilient task primitive and an event-streaming API to
azure-ai-agentserver-core (2.0.0b7), plus a matching set of runnable
samples in azure-ai-agentserver-invocations (1.0.0b6).

The resilient-task primitive is a small, decorator-driven API that lets a
hosted agent run long operations as named tasks that survive process
crashes, out-of-memory kills, and container redeployments. The framework
persists task state and automatically re-invokes in-flight work after a
crash, so the developer doesn't have to write explicit checkpoint or
replay code.

Developer guides:

What ships in this PR

azure-ai-agentserver-core 2.0.0b7

  • Resilient task primitive (azure.ai.agentserver.core.tasks): the
    @task (one-shot) and @multi_turn_task (multi-turn) decorators,
    TaskContext, TaskRun, configurable retries (RetryPolicy),
    cancellation, steering, and a typed exception set.
  • Event-streaming API (azure.ai.agentserver.core.streaming):
    publishes incremental task output to one or more subscribers with
    in-memory and file-backed buffering and live/replay delivery — the
    building block for Server-Sent Events responses a client can
    disconnect from and resume.
from azure.ai.agentserver.core.tasks import task, TaskContext

@task
async def process_document(ctx: TaskContext[dict]) -> dict:
    # ctx.entry_mode is "fresh" | "resumed" | "recovered".
    # After a crash the framework re-invokes from the top with the
    # original ctx.input, so the handler simply picks up again.
    summary = await analyze(ctx.input["document_url"])
    return {"summary": summary}

result = await process_document.run(task_id="doc-42", input={"document_url": "..."})
print(result.output)  # {"summary": "..."}

azure-ai-agentserver-invocations 1.0.0b6

  • Runnable HTTP-host samples that build crash-resilient agents on the
    primitive: resilient_copilot (streaming chat with crash recovery),
    resilient_multiturn (suspend/resume conversation),
    resilient_langgraph (LangGraph integration), and resilient_research
    (multi-stage research loop with checkpointing).
  • Fix: the cancel (POST /invocations/{id}/cancel) and get
    (GET /invocations/{id}) endpoints now resolve the session id
    consistently with the invoke endpoint, so custom handlers can reliably
    look up per-session state.

Out of scope (separate PRs)

This PR is the base of a stack. Follow-on work lives on its own branches:

  • azure-ai-agentserver-responses resilient orchestration —
    branch feature/agentserver-responses-spec016.
  • Deployable hosted-agent demo — branch
    feature/agentserver-durable-agent-demo (temporary showcase; not for
    merge).

Notes for reviewers

  • No behavior changes were made to land this for review; a cleanup pass
    removed incidental formatting churn from unrelated files so the diff
    reflects only the new feature plus version, changelog, and dependency
    updates.
  • The CHANGELOG entries are intentionally short and public-facing; the
    full design lives in the developer guides linked above.

@github-actions github-actions Bot added the Hosted Agents sdk/agentserver/* label May 19, 2026
@RaviPidaparthi
RaviPidaparthi marked this pull request as ready for review May 19, 2026 19:21
@RaviPidaparthi
RaviPidaparthi requested a review from ankitbko as a code owner May 19, 2026 19:21
Copilot AI review requested due to automatic review settings May 19, 2026 19:21
@RaviPidaparthi
RaviPidaparthi requested a review from vangarp as a code owner May 19, 2026 19:21
@RaviPidaparthi RaviPidaparthi changed the title feat(agentserver): pluggable StreamHandler protocol and stream_handler_factory feat(agentserver): Durable Task Framework for azure-ai-agentserver-core May 19, 2026

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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Implements spec-009’s “pluggable stream handler” work for the durable task framework by introducing a StreamHandler protocol with a default QueueStreamHandler, plus related durable-task capabilities (retry, resume route, metadata, samples/tests) and extensive formatting/tidying across tests and samples.

Changes:

  • Added a pluggable streaming abstraction (StreamHandler, QueueStreamHandler, factory type) and wired it into TaskContext.stream() and TaskRun async iteration.
  • Introduced/expanded durable-task building blocks: TaskResult, RetryPolicy, resume HTTP route, hosted provider client, lease renewal helper, and substantial new test coverage + samples.
  • Updated docs/changelogs and reformatted various tests/samples for style consistency.

Reviewed changes

Copilot reviewed 88 out of 92 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
sdk/agentserver/azure-ai-agentserver-invocations/tests/test_tracing_e2e.py Formatting-only adjustments (line wrapping/blank lines).
sdk/agentserver/azure-ai-agentserver-invocations/tests/test_session_id.py Formatting-only adjustments (blank lines, wrapped AsyncClient context).
sdk/agentserver/azure-ai-agentserver-invocations/tests/test_server_routes.py Formatting-only adjustments (blank lines).
sdk/agentserver/azure-ai-agentserver-invocations/tests/test_request_limits.py Minor whitespace cleanup.
sdk/agentserver/azure-ai-agentserver-invocations/tests/test_request_id.py Formatting-only adjustments.
sdk/agentserver/azure-ai-agentserver-invocations/tests/test_multimodal_protocol.py Minor whitespace cleanup and section spacing.
sdk/agentserver/azure-ai-agentserver-invocations/tests/test_invoke.py Formatting-only adjustments (blank lines).
sdk/agentserver/azure-ai-agentserver-invocations/tests/test_graceful_shutdown.py Formatting + wrapped long asserts for readability.
sdk/agentserver/azure-ai-agentserver-invocations/tests/test_get_cancel.py Minor whitespace cleanup.
sdk/agentserver/azure-ai-agentserver-invocations/tests/test_edge_cases.py Formatting-only adjustments (blank lines).
sdk/agentserver/azure-ai-agentserver-invocations/tests/test_decorator_pattern.py Formatting (wrapped JSONResponse returns).
sdk/agentserver/azure-ai-agentserver-invocations/samples/streaming_invoke_agent/streaming_invoke_agent.py Reformatted token list for readability.
sdk/agentserver/azure-ai-agentserver-invocations/samples/simple_invoke_agent/simple_invoke_agent.py Minor whitespace cleanup.
sdk/agentserver/azure-ai-agentserver-invocations/samples/multiturn_invoke_agent/multiturn_invoke_agent.py Formatting; JSONResponse construction wrapped.
sdk/agentserver/azure-ai-agentserver-invocations/samples/durable_multiturn/store.py New sample persistence helper (file-backed JSON store).
sdk/agentserver/azure-ai-agentserver-invocations/samples/durable_multiturn/requirements.txt New sample requirements.
sdk/agentserver/azure-ai-agentserver-invocations/samples/durable_multiturn/app.py New durable multiturn sample host wiring.
sdk/agentserver/azure-ai-agentserver-invocations/samples/durable_multiturn/agent.py New durable multiturn sample agent task.
sdk/agentserver/azure-ai-agentserver-invocations/samples/durable_langgraph/store.py New sample persistence helper (file-backed JSON store).
sdk/agentserver/azure-ai-agentserver-invocations/samples/durable_langgraph/requirements.txt New sample requirements (LangGraph + deps).
sdk/agentserver/azure-ai-agentserver-invocations/samples/durable_langgraph/app.py New streaming + steering durable LangGraph host sample.
sdk/agentserver/azure-ai-agentserver-invocations/samples/durable_copilot/store.py New sample persistence helper (file-backed JSON store).
sdk/agentserver/azure-ai-agentserver-invocations/samples/durable_copilot/requirements.txt New sample requirements (Copilot SDK, core, Starlette, uvicorn).
sdk/agentserver/azure-ai-agentserver-invocations/samples/durable_copilot/app.py New durable Copilot host sample with SSE.
sdk/agentserver/azure-ai-agentserver-invocations/samples/durable_copilot/agent.py New steerable durable Copilot agent sample.
sdk/agentserver/azure-ai-agentserver-invocations/samples/durable_claude/store.py New sample persistence helper (file-backed JSON store).
sdk/agentserver/azure-ai-agentserver-invocations/samples/durable_claude/requirements.txt New sample requirements (Anthropic SDK + runtime deps).
sdk/agentserver/azure-ai-agentserver-invocations/samples/durable_claude/app.py New durable Claude host sample with SSE.
sdk/agentserver/azure-ai-agentserver-invocations/samples/durable_claude/agent.py New steerable durable Claude agent sample.
sdk/agentserver/azure-ai-agentserver-invocations/samples/async_invoke_agent/async_invoke_agent.py Formatting-only adjustments (wrapped JSON dict literals).
sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md Changelog updates to mention durable samples + dependency bump.
sdk/agentserver/azure-ai-agentserver-core/tests/test_tracing.py Formatting-only adjustments.
sdk/agentserver/azure-ai-agentserver-core/tests/test_startup_logging.py Formatting-only adjustments and wrapped long lines.
sdk/agentserver/azure-ai-agentserver-core/tests/test_server_routes.py Minor whitespace cleanup.
sdk/agentserver/azure-ai-agentserver-core/tests/test_logger.py Minor whitespace cleanup.
sdk/agentserver/azure-ai-agentserver-core/tests/test_graceful_shutdown.py Formatting-only adjustments and wrapped long asserts.
sdk/agentserver/azure-ai-agentserver-core/tests/test_config.py Formatting for long function signatures.
sdk/agentserver/azure-ai-agentserver-core/tests/durable/test_task_result.py New tests for TaskResult wrapper behavior + guardrails.
sdk/agentserver/azure-ai-agentserver-core/tests/durable/test_streaming.py New tests for pluggable stream handler integration.
sdk/agentserver/azure-ai-agentserver-core/tests/durable/test_source.py New tests exercising source field persistence.
sdk/agentserver/azure-ai-agentserver-core/tests/durable/test_retry.py New tests for RetryPolicy and retry integration.
sdk/agentserver/azure-ai-agentserver-core/tests/durable/test_resume_route.py New tests for the resume HTTP route behavior.
sdk/agentserver/azure-ai-agentserver-core/tests/durable/test_models.py New tests for durable models/exceptions.
sdk/agentserver/azure-ai-agentserver-core/tests/durable/test_metadata.py New tests for dict-like TaskMetadata + flush semantics.
sdk/agentserver/azure-ai-agentserver-core/tests/durable/test_local_provider.py New tests for local durable provider CRUD/listing.
sdk/agentserver/azure-ai-agentserver-core/tests/durable/test_lifecycle.py New lifecycle automation tests.
sdk/agentserver/azure-ai-agentserver-core/tests/durable/test_get.py New tests for DurableTask.get().
sdk/agentserver/azure-ai-agentserver-core/tests/durable/test_entry_mode.py New tests for ctx.entry_mode across paths.
sdk/agentserver/azure-ai-agentserver-core/tests/durable/test_decorator.py New tests for @durable_task decorator/options/type extraction.
sdk/agentserver/azure-ai-agentserver-core/tests/durable/test_cancellation_timeout.py New tests for cancellation, timeout, and termination.
sdk/agentserver/azure-ai-agentserver-core/tests/durable/test_callable_factories.py New tests for callable factories on tags/description.
sdk/agentserver/azure-ai-agentserver-core/tests/durable/init.py New package init for durable tests.
sdk/agentserver/azure-ai-agentserver-core/tests/conftest.py Formatting-only adjustments.
sdk/agentserver/azure-ai-agentserver-core/samples/durable_streaming/requirements.txt New durable sample requirements.
sdk/agentserver/azure-ai-agentserver-core/samples/durable_streaming/durable_streaming.py New sample demonstrating streaming with durable tasks.
sdk/agentserver/azure-ai-agentserver-core/samples/durable_source/requirements.txt New durable sample requirements.
sdk/agentserver/azure-ai-agentserver-core/samples/durable_source/durable_source.py New sample demonstrating source usage.
sdk/agentserver/azure-ai-agentserver-core/samples/durable_retry/requirements.txt New durable sample requirements.
sdk/agentserver/azure-ai-agentserver-core/samples/durable_retry/durable_retry.py New sample demonstrating retry policies.
sdk/agentserver/azure-ai-agentserver-core/pyproject.toml Added httpx dependency + optional hosted extras (azure-identity).
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/_stream.py New StreamHandler protocol + default QueueStreamHandler + factory alias.
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/_run.py New TaskRun async-iter streaming integration and lifecycle control methods.
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/_retry.py New RetryPolicy implementation and presets.
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/_resume_route.py New Starlette route for POST /tasks/resume.
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/_result.py New TaskResult wrapper class.
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/_provider.py New storage provider protocol for durable subsystem.
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/_metadata.py New dict-like TaskMetadata with flush/auto-flush.
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/_lease.py New lease identity utilities + renewal loop.
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/_exceptions.py New durable exception types (failed/suspended/cancelled/etc.).
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/_context.py New TaskContext with stream support and lifecycle fields.
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/_client.py New hosted durable task provider httpx client.
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/durable/init.py New public durable API exports.
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_middleware.py Formatting-only adjustments for imports/log calls.
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_errors.py Minor formatting simplification.
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/_config.py Minor formatting simplification.
sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/init.py Minor whitespace cleanup.
sdk/agentserver/azure-ai-agentserver-core/README.md Added durable-task documentation section + link.
sdk/agentserver/azure-ai-agentserver-core/CHANGELOG.md Large changelog entry documenting durable subsystem and other changes.
sdk/agentserver/.gitignore Added .vscode/ ignore.
Comments suppressed due to low confidence (1)

sdk/agentserver/azure-ai-agentserver-invocations/samples/durable_multiturn/store.py:1

  • For JSON persistence, it’s better to write/read with an explicit encoding (UTF-8) for cross-platform consistency. Consider using open(fd, \"w\", encoding=\"utf-8\") (or os.fdopen) and also using read_text(encoding=\"utf-8\") in load() to avoid platform-default encoding surprises.

Comment thread sdk/agentserver/azure-ai-agentserver-core/CHANGELOG.md Outdated
@RaviPidaparthi RaviPidaparthi changed the title feat(agentserver): Durable Task Framework for azure-ai-agentserver-core feat(agentserver): Durable Tasks for azure-ai-agentserver-core May 19, 2026
RaviPidaparthi and others added 21 commits May 30, 2026 00:01
Lands the RED tests for User Story 3 (public API cleanup) per FR-030 RED-first.

T013 (test_decorator.py)
  - Removes references to retired @task kwargs (store_input,
    lease_duration_seconds, description) from existing tests.
  - Drops TaskOptions from the public import (demoted in __all__).
  - Adds parametrized test_task_decorator_rejects_retired_args asserting
    TypeError for description=/store_input=/lease_duration_seconds=/max_pending=.
  - Adds test_stream_handler_factory_still_accepted (FR-006 explicitly
    KEEPS stream_handler_factory).

T014 (test_models.py)
  - Removes TaskSuspended import + test_task_suspended_reason.
  - Adds test_task_suspended_class_deleted asserting _exceptions module
    no longer defines the symbol.

T015 (test_steering.py)
  - Removes ctx.previous_input reads from test_steered_context_fields.
  - Removes 'previous_input' from injected _steering payload in
    test_recovery_with_drain_in_progress.
  - Renames ctx.generation -> ctx.steering_generation in test asserts.
  - Adds test_task_context_previous_input_removed + new-name field tests.

T016 (test_public_api_surface.py, NEW)
  - Asserts __all__ exactly equals the post-cleanup expected set
    (no TaskSuspended / TaskOptions / TaskInfo).
  - Asserts Task.get / Task.list are not public (renamed to _get/_list).

T017 (test_entry_mode.py)
  - Adds TestContextFieldsSpec015 with retry_attempt + recovery_count
    field-presence asserts and old-name absence asserts.

Verified RED:
  pytest -k rejects_retired_args or test_task_suspended_class_deleted or
         test_public_all_matches or test_retired_symbols or
         test_task_get_list
  -> 8 failed (decorator+models+public_api_surface)

  pytest -k test_task_context_previous_input_removed or
         test_task_context_steering_generation_field_present or
         test_task_context_retry_attempt_field_present or
         test_task_context_recovery_count_field_present
  -> 4 failed (entry_mode+steering)

  Total 12 new RED + 18 still-RED from contract_completeness meta-test.

GREEN implementation lands in the next commit (T018-T027b).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the public-API cleanup user story (US3 / FR-006, FR-007, FR-008)
on top of the Phase 3 RED tests committed in 9a8eedd.

Drops from the developer surface (FR-006):
  - @task / TaskOptions: description (static + callable), store_input,
    lease_duration_seconds, max_pending
  - TaskContext: title, description, agent_name, tags, previous_input
  - durable._exceptions.TaskSuspended (class deleted entirely)
  - durable.__init__ __all__: TaskSuspended, TaskOptions, TaskInfo

Renames on TaskContext (FR-007):
  - run_attempt          -> retry_attempt
  - lease_generation     -> recovery_count
  - generation           -> steering_generation

Demotes (FR-008):
  - TaskCallable.get / TaskCallable.list -> _get / _list (internal only)
  - TaskOptions -> internal (no longer in __all__)
  - TaskInfo    -> internal (no longer in __all__)

Storage changes:
  - payload["input"] is always written (store_input gate removed at
    TaskManager.create_and_start). Callers never opted out in practice.
  - _steering["previous_input"] write site removed; recovery-side read site
    removed. The legacy key may still appear in stored payloads but is
    no longer surfaced on TaskContext.

Framework constants extracted (no developer knob):
  - durable._manager._DEFAULT_LEASE_SECONDS = 60
  - durable._decorator._DEFAULT_MAX_PENDING_STEERING = 10

Cascading test/sample updates:
  - tests/durable: test_callable_factories, test_get, test_retry,
    test_sample_e2e, test_steering, test_stream_handler, test_streaming
    updated to use renamed fields and internal _get/_list.
  - samples/durable_retry: ctx.retry_attempt rename.
  - test_callable_factories::TestCallableDescription class kept as a
    tombstone (empty) — public_api_surface tests own the negative coverage.
  - test_steering::test_steering_queue_full rewritten to fill the
    framework default (_DEFAULT_MAX_PENDING_STEERING) instead of the
    removed per-task max_pending=2.
  - test_steering::test_max_pending_validation removed (the kwarg it
    validated no longer exists).

Verification (Phase 3 exit criteria):
  - tests/durable/: 261 passed, 8 failed.
    All 8 failures are in test_contract_completeness.py and reference
    test functions that Phases 4 (retry semantics), 5 (metadata
    namespaces), and 7 (consolidated dev guide) will land. Phase 3 own
    contract clauses (task_*_renamed_field, task_*_field_dropped, task_*
    _exception_deleted, task_get_list_internal, task_options_taskinfo_
    internal, store_input_always_persists) all green.
  - Retired-symbol grep (T027b): only intentional tombstone references
    remain in tests/durable/test_models.py, test_steering.py,
    test_entry_mode.py, test_public_api_surface.py, and
    test_contract_completeness.py.
  - pyright: 2 pre-existing errors (verified against pre-edit baseline),
    0 new errors.
  - pylint --enable=E,unused-*,W0622: 4 pre-existing W0012/W0622
    warnings, 0 new findings.

Docs (durable-task-overview.md, durable-task-developer-guide.md,
CHANGELOG.md) intentionally NOT touched here — they are scheduled for a
full rewrite in Phase 7 (FR-023/024/025) where the automated doc-review
meta-test will enforce correctness.

Spec: 015-core-task-primitive-and-sample-fixups
Branch: feature/agentserver-durable-tasks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the failing-by-design tests that pin the cross-lifetime contract
for ``ctx.retry_attempt`` and ``RetryPolicy.max_attempts``:

FR-001  ``ctx.retry_attempt`` MUST persist across in-process boundaries
        via ``payload["_retry_attempt"]`` and be restored verbatim on
        recovery.
FR-002  ``RetryPolicy.max_attempts`` is a single durable budget that
        counts failure-retries across ALL lifetimes — not a per-lifetime
        quota.
FR-003  Crash recovery MUST NOT consume any of the retry budget. Only
        handler-raised exceptions consume it.

New tests in tests/durable/test_retry.py (TestRetryAttemptDurability):
  - test_retry_attempt_cross_lifetime_durability
  - test_retry_attempt_budget_exhausts_across_crash
  - test_crash_recovery_does_not_consume_retry_budget
  - test_steering_drain_resets_retry_attempt_to_zero

New tests in tests/durable/test_entry_mode.py (TestRecoveryRetryAttempt):
  - test_recovered_handler_sees_persisted_retry_attempt
  - test_recovery_entry_mode_does_not_increment_retry_attempt

All 6 tests fail RED today for the same root cause: the manager
hardcodes ``retry_attempt=0`` at every ``TaskContext`` construction
site, ignoring the payload-persisted counter. Phase 4 GREEN
(T030-T033) wires reads + writes + budget enforcement.

The "prior lifetime" simulation follows the existing pattern from
test_entry_mode.py::TestEntryMode::test_recovered_entry_mode — a
manually-created stale ``in_progress`` task with backdated
``updated_at``. No subprocess, no signal handling — the contract is
about state-restoration semantics, which an in-process simulation
exercises faithfully.

Contract enforcer pass: tests/durable/test_contract_completeness.py
now reports 5 failures (was 8). The 3 newly-paired clauses:
  - retry_attempt_cross_lifetime_durability
  - retry_budget_exhausts_across_crash
  - crash_recovery_does_not_consume_retry_budget

Remaining 5 failures are all Phase 5 (metadata namespaces) and
Phase 7 (consolidated dev guide) work, as expected.

Branch: feature/agentserver-durable-tasks
Spec:   015-core-task-primitive-and-sample-fixups

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the cross-lifetime retry contract pinned by Phase 4 RED
(0e3db4e):

  FR-001  ctx.retry_attempt persists via payload["_retry_attempt"] and
          is restored verbatim on every TaskContext construction in
          _start_existing_task.
  FR-002  RetryPolicy.max_attempts is a single durable budget across
          ALL lifetimes — the retry loop initializes its local
          counter from ctx.retry_attempt instead of resetting to 0.
  FR-003  Crash recovery does NOT consume the budget. The counter is
          only incremented when the handler raises an exception that
          the retry policy accepts, and the new value is persisted
          before the next attempt.

Changes in _manager.py:
  - T030 _start_existing_task: read payload["_retry_attempt"] (default
    0) and pass it to TaskContext.retry_attempt. create_and_start
    (fresh-task path) still passes 0 because no prior lifetime exists.
  - T031 _execute_task_loop initial state: ``attempt = ctx.retry_attempt``
    so the loop honors the cross-lifetime counter.
  - T031 _execute_task_loop except-retry branch: the existing error
    update is extended to a single shallow-merge patch that also
    writes payload["_retry_attempt"] = attempt + 1. A subsequent
    crash + recover restores the bumped value via T030.
  - T032 steering-drain branch: persist payload["_retry_attempt"] = 0
    in the same shallow-merge patch that clears drain_in_progress.
    Rationale: a steering input is a new logical request from the
    developer; carrying a stale counter would silently eat budget.
    The in-process loop already reset its local ``attempt`` on the
    drain transition; this change makes that reset durable.

Changes in _retry.py:
  - T034 RetryPolicy.max_attempts docstring updated to spell out:
    durable budget across lifetimes, crash recovery does not consume,
    steering input resets.

Verification:
  - tests/durable/ : 270 passed, 5 failed.
  - The 6 Phase 4 RED tests committed in 0e3db4e all turn GREEN
    on first run (no edits needed in the test files).
  - The remaining 5 failures are intentional Phase 5 (metadata
    namespaces, 4 tests) and Phase 7 (consolidated dev guide, 1 test).
  - test_contract_completeness.py:
      retry_attempt_cross_lifetime_durability       PASS
      retry_budget_exhausts_across_crash            PASS
      crash_recovery_does_not_consume_retry_budget  PASS
  - pyright on changed files: 0 errors, 0 warnings.

Branch: feature/agentserver-durable-tasks
Spec:   015-core-task-primitive-and-sample-fixups

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 11 RED tests for TaskMetadata named-namespace facility:

- test_default_namespace_callable_and_dict (FR-004)
- test_named_namespace_isolation (FR-003)
- test_named_namespace_auto_vivifies
- test_namespaces_have_independent_dirty_tracking
- test_flush_per_namespace_only (FR-003) [callback signature changes
  to (namespace, data)]
- test_lifecycle_boundary_snapshots_all_touched_namespaces [new
  flush_all() API]
- test_no_cross_namespace_pollution_after_delete
- test_underscore_namespace_not_enforced_by_primitive (FR-005)
- test_metadata_module_has_no_autoflush_symbols [source-scan: enforces
  T037 deletes start_auto_flush, stop_auto_flush, _auto_flush_loop,
  _flush_task, _flush_interval]
- test_default_namespace_has_no_framework_keys
- test_named_namespace_survives_recovery_with_independent_state [T036
  in-process variant; real-crash subprocess harness deferred to
  Phase 8 deliverable]

All 11 fail RED for the right reason: TaskMetadata is not callable
yet; autoflush symbols still present. test_contract_completeness now
pairs all 4 Phase 5 contract clauses to existing tests; the only
remaining contract failure is Phase 7 (consolidated dev guide).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rewrite _metadata.py: TaskMetadata.__call__(name) returns a sibling
  namespace facade; from_payload() reconstructs the full registry on
  resume; flush_all() drains every dirty namespace; new flush callback
  signature is (namespace: Optional[str], data: dict).
- Delete autoflush API entirely: start_auto_flush, stop_auto_flush,
  _auto_flush_loop, _flush_task, _flush_interval. Lifecycle boundaries
  in the manager now drive explicit flushes (FR-003).
- _manager.py: resume path uses TaskMetadata.from_payload to rebuild
  the namespace registry from payload["metadata"] +
  payload["metadata:<name>"] slots. _make_metadata_flush persists
  per-namespace to the matching slot. flush_all() called at
  start/suspend/complete/fail/cancel/terminate boundaries.
- _decorator.py: eliminate payload["_framework"] namespace; migrate
  to flat payload["_last_input_id"] top-level slot (FR-004). _* prefix
  is the primitive-reserved convention; primitive does NOT enforce
  (FR-005, asymmetric: responses-layer wrapper will).
- 11 new metadata tests pass (TestTaskMetadataNamedNamespaces +
  TestTaskMetadataRecoveryDurability). 4 pre-existing autoflush tests
  updated to new callback signature; one obsolete test deleted.
- Full durable suite: 284 pass / 1 fail (Phase 7 doc test, expected).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update existing tests for spec 015 FR-040 contract changes:
- run_attempt → retry_attempt rename in all DurabilityContext fixtures
- Delete test_task_stores_input (option removed in FR-006)
- Add test_task_input_is_not_stored_via_decorator_option (asserts the
  store_input attr is gone from TaskOptions)
- Rewrite test_metadata_hides_framework_keys →
  test_metadata_rejects_underscore_prefixed_keys (FR-005)
- Add test_metadata_is_callable_for_named_namespace (FR-003)
- Add test_named_namespace_also_rejects_underscore_prefix (FR-005
  asymmetric enforcement on handler-facing wrapper)

22 tests fail RED — pending GREEN in T046-T056b.

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

- Rewrite _durability_context.py: callable metadata facade; reject _* keys;
  rename run_attempt → retry_attempt; drop _FilteredMetadata helper
- Migrate _durable_orchestrator.py to ctx.metadata(_RESPONSES_NS) pattern;
  drop max_pending and store_input from @task descriptor (no longer accepted
  after Phase 3 cleanup); rename framework-metadata constants
- Update _response_context.py to use retry_attempt= ctor kwarg
- Migrate all responses-side test fixtures: callable metadata stub
  (_FakeTaskMetadata) replacing plain-dict fixture; drop _FilteredMetadata
  imports/wrappers; rename run_attempt → retry_attempt repo-wide

Tests:
- responses unit: 601 pass
- responses e2e (non-live): 218 pass / 1 skipped
- core durable: 284 pass / 1 fail (Phase 7 doc gate, expected)

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

- specs/durability-contract.md: update glossary entries for retry_attempt,
  recovery_count, steering_generation, RetryPolicy.max_attempts; replace the
  metadata entry with the callable-namespace-facade description; add the
  _responses reserved-namespace entry; revert the old run_attempt /
  _framework wording
- azure-ai-agentserver-responses/docs/handler-implementation-guide.md and
  durable-responses-developer-guide.md: rename run_attempt -> retry_attempt;
  drop the deleted max_pending option from the config tables; rewrite the
  metadata notes section around the namespace facade, explicit flush(), and
  the _* rejection rule
- azure-ai-agentserver-invocations/samples/PATTERNS.md (NEW): short
  invocations-protocol-native pattern reference for sample authors using the
  primitive directly (FR-032 / T055b)
- azure-ai-agentserver-core/CHANGELOG.md + azure-ai-agentserver-responses/CHANGELOG.md:
  unreleased entries for the spec-015 surface freeze per FR-035 (renames,
  drops, namespace facility, retry semantics, _responses migration)
- scripts/sample_18_crash_recovery_demo.py: opportunistic local script

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Six checks land RED until Phase 7 GREEN creates docs/durable-task-guide.md:
- test_every_public_symbol_is_referenced_in_guide (FR-024 #1)
- test_pr_blocking_failure_mode_for_undocumented_symbol (FR-025)
- test_removed_names_absent_from_guide (FR-024 #2)
- test_required_sections_present_in_order (FR-024 #3)
- test_canonical_statements_present (FR-024 #4)
- test_no_internal_contradictions (FR-024 #5)

The T058 regression sub-test (test_pre_consolidation_state_would_have_failed)
already passes — it proves the same checks bite when run against a synthetic
pre-consolidation 'guide' that contains retired names, missing sections,
and undocumented symbols.

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

NEW docs/durable-task-guide.md (~600 lines, 8-section learning arc):
- §1 Why — the entrypoint-gap framing
- §2 Mental Model — invariant, state-bucket diagram, entry-mode table
- §3 Hello World — minimal example
- §4 Concepts — retry semantics, callable namespace facade, suspend/
  resume/steering, streaming, results, preconditions, exceptions
- §5 Reference — full @task / Task / TaskContext / TaskMetadata /
  RetryPolicy / exception tables
- §6 Patterns — 5 copy-and-ship idioms (at-most-once, watermark
  iteration, multi-turn, named namespaces, streaming)
- §7 Operational / Testing — lifecycle boundary table, counters,
  local dev, crash-harness pointer
- §8 What This Is NOT — explicit non-goals (replay, workflow engine,
  bulk store, queue)
- Rename-map appendix (FR-007)

Old guides converted to 10-line redirects to preserve incoming links.

Meta-test test_dev_guide_review.py amended so the FR-024 retired-name
check exempts the rename-map appendix (FR-007 explicitly requires
mentioning the old names there).

Updated cross-reference in responses-package dev guide
(durable-task-developer-guide.md → durable-task-guide.md).

Tests: 292 pass / 0 fail (was 291 pass / 1 fail under the Phase 7
RED). The doc-existence gate in test_contract_completeness.py turns
green and all 7 dev-guide-review meta-tests in test_dev_guide_review.py
pass.

T057-T063 done.

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

Adds tests/test_durable_samples_structure.py — 27 parametrized
assertions covering FR-010 / FR-011 / FR-013 / FR-014 / SC-004:

- Each of the 4 canonical durable samples (durable_copilot,
  durable_langgraph, durable_multiturn, durable_research) must exist
  and ship agent.py + app.py + README.md + requirements.txt.
- durable_claude must be removed.
- No sample's Python sources may reference Phase 3-6 retired names
  (ctx.generation, ctx.run_attempt, etc.).
- durable_copilot/agent.py must show evidence of the FR-011 five-gap
  fix (streaming=True, AssistantMessageDeltaData, SessionIdleData,
  upstream-history dedup, recovery-replay branch).
- durable-agent-demo must remain structurally intact (T073 promise).

Land RED: 11 failed / 16 passed. The 11 failures are exactly the work
items for Phase 8 GREEN (T068-T073).

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

- T068 durable_copilot: rewrite to close 5 FR-011 gaps
  * streaming=True wired into create_session + resume_session
  * AssistantMessageDeltaData -> ctx.stream({type: text_delta}) chunks
  * SessionIdleData -> ctx.stream({type: session_idle}) chunk
  * Upstream-history dedup via session.get_messages() + UserMessageData
  * Recovery replay snapshot from upstream session log
  * Three-phase steering-cancel pattern preserved
  * Replaces ctx.generation with ctx.steering_generation (FR-007)

- T069 durable_langgraph: verified no rewrite needed
  Existing implementation already matches Phase 0 Q1 design:
  one @task + LangGraph SqliteSaver/thread_id, NO DurabilityContext.

- T070 durable_multiturn: rewrite to use named-namespace metadata
  * ctx.metadata (default) -> per-invocation state
  * ctx.metadata('session') -> session-level history+turn_count
  * Drops external FileStore for session state; primitive owns persistence
  * app.py poll handler reads from session_workflow.get(task_id).payload

- T071 NEW durable_research: peer-sample-shape distillation of
  durable-agent-demo/src/durable-research-agent
  * 12-stage research loop with checkpoint-and-resume
  * ctx.metadata watermark for completed_stages + results
  * Live SSE streaming + async-poll modes
  * Drops supervisor/entrypoint scaffolding of the reference demo

- T072 durable_claude: deleted (consumer-only design choice)

- T073 durable-agent-demo: verified untouched (no diff vs HEAD)

Structural gate: 23/27 pass; remaining 4 failures are missing
README.md files which will be created in Phase 9.

Refs: spec 015 FR-007 FR-010 FR-011 FR-012; Phase 0 Q1 Q2 Q2b.

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

Adds tests/test_samples_shippable_bar.py as the static contract test
for per-sample shippable bar (US6) and operational guidance (US7):

- samples/SHIPPABLE.md exists and lists every required sample (FR-013).
- samples/DURABLE_SAMPLES.md exists and links each per-sample
  README (FR-021).
- Each sample/README.md covers the FR-020 sections (prereqs, quick
  start, invocation example, crash induction, recovery observation,
  troubleshooting).
- Each sample/requirements.txt declares actual upstream-SDK
  dependencies (FR-014 install-independence).

Run state (RED): 2 failed / 4 passed / 26 skipped. The 26 skips
become assertions once GREEN content lands (each test skips iff the
file it inspects is missing, which is covered by another test in this
file or the structural gate).

Refs: spec 015 FR-013 FR-014 FR-020 FR-021; US6, US7.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- T076-T079: per-sample README.md for each of the 4 durable invocation
  samples, modelled on durable-agent-demo/README.md but trimmed to
  peer-sample shape. Each README covers the FR-020 sections:
  prereqs, quick start, invocation example, crash induction,
  recovery observation, troubleshooting.

- T080: samples/DURABLE_SAMPLES.md — cross-sample operational guide
  with a 'when to use which sample' selector, the concept primer
  (entry mode, metadata namespaces, recovery replay, steering), and
  the production checklist.

- T082: samples/SHIPPABLE.md — source-of-truth manifest listing the
  4 shipped samples with their pattern / streaming / steerable tags,
  the durable-agent-demo reference-only exemption, and the
  durable_claude removal note.

- T083: per-sample requirements.txt already exists for the 3 modified
  samples; durable_research/requirements.txt landed in Phase 8.

- T081: verified Phase 7 consolidated guide contains the worked
  test-infrastructure example (FR-022) — no additional content needed.

- T075: regex tightened in test_samples_shippable_bar.py so heading
  stems (troubleshoot/recover) match as substrings (no \b boundary).

Phase 9 gates green: 27/27 structural + 32/32 shippable-bar.

Refs: spec 015 FR-013 FR-014 FR-020 FR-021 FR-022; US6, US7.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Core CHANGELOG: add Documentation section for new docs/durable-task-guide.md
  consolidated guide + tests/durable/test_dev_guide_review.py CI doc-review
  meta-test (Phase 7 T058). Update stale guide-path references ("durable-task-
  developer-guide.md" -> "durable-task-guide.md") in the Features-Added and
  Bugs-Fixed entries that pre-dated Phase 7. Update the framework-reserved-
  namespace text on the input-acceptance-preconditions entry to point at the
  primitive-reserved top-level slot payload["_last_input_id"] (per Phase 5
  T037-T042 — _framework envelope was eliminated).

- Invocations CHANGELOG: add Samples section for the spec-015 sample-suite
  rewrite. durable_copilot (5 FR-011 gaps closed; commit 59ad1f6),
  durable_multiturn (named-namespace metadata), durable_langgraph (verified),
  durable_research (new peer-sample), durable_claude (removed). New
  samples/SHIPPABLE.md + samples/DURABLE_SAMPLES.md manifests + per-sample
  READMEs. tests/test_samples_shippable_bar.py CI gate (32 assertions).

Phase 10 closeout artifacts (in gitignored specs/015 dir):
- post-mortem.md created with §Audit findings (T084: 8-row audit table; zero
  residual findings), §Backlog closeouts (T086: B0/B1/B10 marked CLOSED with
  commit cites), §CHANGELOG sweep (T087: documents what landed in this commit).
- backlog.md: B0 (auto-flush) marked CLOSED with Phase 5 cite; B1 (invocations
  samples) marked CLOSED with Phase 8 commit 59ad1f6 cite; B10 (run_attempt
  cross-lifetime) marked CLOSED with Phase 4 commit b9a3584 cite.

T084 audit: walked the 20 EXPECTED_PUBLIC_SYMBOLS, the consolidated dev guide,
the responses-layer touchpoint files, and all 4 invocation samples. No residual
gaps surfaced — all of FR-009 audit landed in Phases 3-9. T085: no new backlog
entries needed.

Branch: feature/agentserver-durable-tasks
Refs: spec 015 Phase 10 (US9 closeout); FR-009 (audit), FR-035 (changelog)

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

T026 (close): the dev-guide already documents the public exception table
(durable-task-guide.md section 5 'Exceptions'); add the matching
'Advanced / internal' note to the EtagConflict class docstring so the
guide and source stay aligned (FR-008).

T088-T094 (verification): three-suite verification per SC-009.
- Responses (excl. live Copilot): 1306 pass / 1 skip (309s).
- Responses durability conformance: 37 pass (178s).
- Core durable + completeness: 382 pass / 5 skip / 2 pre-existing
  TestSigtermHandler failures from spec014 (verified at 2127bef).
- Invocations e2e: 245 pass / 2 skip.
- Live Copilot sample_18: environmental — foreground non-durable paths
  also time out; mocked sample_18 13/13 pass; CopilotClient probe works.

Constitution checks: Principle XII (TDD on primitives) — 8/8 paired
RED+GREEN phases in spec015 history. Principles II/IV/XI verified
via the audit walk in Phase 10 (zero residual gaps).

All 13 success criteria PASS (SC-009 with live-upstream caveat).
Spec 015 — CLOSED.

Branch: feature/agentserver-durable-tasks
Spec: 015-core-task-primitive-and-sample-fixups
FR: FR-008 (typed exceptions surfaced in guide)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The end-user-developer guide at docs/durable-task-guide.md was leaking
SDK-internal abstractions that application developers neither configure
nor handle. Fix this and lock the cleanup with the doc-review meta-test.

Changes to the guide:

* TaskStatus: now correctly documented as the four lifecycle states
  from the foundry container spec (SOT): pending / in_progress /
  suspended / completed. Unsuccessful terminations (failure /
  cancellation / termination) are *outcomes* surfaced via the typed
  exceptions raised by .run() / .result() — they are not separate
  stored statuses. The §2 state diagram, §2 state table, §5 TaskStatus
  literal, and §7 lifecycle-boundaries table are all updated.
* §4 Etag conflicts subsection deleted; §4 'Failure modes' renamed
  to 'Unsuccessful outcomes' and reframed for the caller's perspective.
* §7 'Local development' rewritten: zero-configuration. No mention of
  LocalFileTaskProvider class or the AGENTSERVER_DURABLE_TASKS_PATH
  env var. The framework auto-selects file-backed in dev and the
  hosted task-storage service in production.
* §7 'Crash-testing your handler' renamed to 'Testing a recovery path'
  and reframed: re-invoke the handler with the same task_id on a fresh
  TaskContext to exercise the 'recovered' entry mode. The SDK's own
  _crash_harness is no longer referenced.
* §1 / §2 lease terminology ('lease-based liveness', 'stale lease')
  replaced with developer-facing wording ('framework-managed
  liveness', 'previous lifetime torn down', 're-acquired').
* §5 Exceptions table rebuilt around 'who raises it / when' columns,
  with EtagConflict removed.

Public-surface change:

* EtagConflict is removed from durable/__init__.py.__all__. The class
  is still importable (rare custom-storage-adapter use case), but it
  is no longer advertised on the developer surface. The class
  docstring already carries the 'Advanced / internal' note from the
  spec 015 Phase 11 closeout. tests/durable/test_contract_completeness.py
  and tests/durable/test_public_api_surface.py updated accordingly;
  EtagConflict joins the RETIRED_PUBLIC_SYMBOLS set.

Meta-test extension:

* tests/durable/test_dev_guide_review.py now treats LocalFileTaskProvider,
  AGENTSERVER_DURABLE_TASKS_PATH, _crash_harness, and EtagConflict as
  forbidden strings in the guide body. The guide will fail CI if any
  of these reappear.

CHANGELOG entry added documenting the cleanup.

Branch: feature/agentserver-durable-tasks
Spec: 015-core-task-primitive-and-sample-fixups (Phase 11 follow-up)
Source-of-truth cite: /tmp/foundrysdk_specs/specs/hosted-agents/container-spec/docs/durable-task-spec.md line 507 (TaskStatus literal)

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

Follow-up review of docs/durable-task-guide.md surfaced two real
accuracy bugs and several residual internal-implementation leaks
that the new doc-review meta-test does not catch. Fixing them.

Accuracy bugs
-------------

* §5 'Task (the handle)' previously documented the entry-point
  signatures as:
    .run(task_id, input, *, if_last_input_id=None, input_id=None)
    .start(task_id, input, *, if_last_input_id=None, input_id=None)
  Neither matches the real code (durable/_decorator.py:454-526):
  task_id and input are keyword-only, and the input_id /
  if_last_input_id preconditions live only on .start(), not on
  .run(). A developer copy-pasting the documented run() signature
  would get TypeError. Replaced with the actual signatures and an
  explanatory sentence.

* §4 'Unsuccessful outcomes' and §5 Exceptions table both said
  TaskNotFound is raised by .get() / .start(). There is no
  Task.get() — .get() / .list() live on the (non-exported)
  durable._client.Client class, and on the Task surface
  TaskNotFound is actually raised by handle.result() (when the
  referenced task_id has been deleted) and by .start() during
  resume. Rewrote both places to cite the real raisers.

Residual internal leakage
-------------------------

* §4 metadata section leaked the on-disk payload schema
  ('the default namespace lives at payload["metadata"]; named
  namespaces live at payload["metadata:<name>"]'). End-user
  developers consume ctx.metadata(...); they never read the raw
  payload. Deleted the sentence.

* §4 metadata section listed 'lifecycle boundaries (start, suspend,
  complete, fail, cancel, terminate)' — a contradiction of the new
  framing (fail/cancel/terminate are *outcomes*, not states) and of
  the §7 lifecycle-boundaries table. Rewrote to '(task start,
  ctx.suspend(...) return, handler return or unhandled raise)'.

* §5 Task section explicitly referenced 'internal .get() / .list()
  helpers used by tests and the hosting layer; treat those as
  not-for-end-user code'. If they are internal, the public guide
  should not name them. Removed entirely.

* §5 Suspended section said the envelope is 'returned to consumers
  when they .get() a task' — same .get() issue. Rewrote in terms of
  the handler's return-await pattern and TaskResult.suspended.

* §7 Testing-recovery section referenced
  'tests/durable/test_retry.py for a copy-paste cookbook example'.
  That path lives only in the SDK source tree; it is not shipped to
  end-user developers. Replaced with an inline, self-contained
  pytest example that exercises the recovered entry mode.

* §5 RetryPolicy section showed
  'RetryPolicy(max_attempts: int, backoff: ... = ...)' — ellipsis-
  as-placeholder. Replaced with the actual constructor signature
  from durable/_retry.py:52-62 and a short note on retry_on.

Clarity / consistency
---------------------

* §2 TaskStatus 'completed' row read as if cancellation / termination
  *transition the task to* completed. Reframed to make the
  status-vs-outcome split explicit: the status is completed; the
  outcome (success / failure / cancellation / termination) is
  surfaced through .run()/.result() per §4.

* §3 inline code comment for .run() was missing the 'or with a
  task_id whose previous run already completed' qualifier that the
  real _decorator.py:474-475 docstring carries; surrounding prose
  did not mention the completed-collision case either. Both updated
  to match the real lifecycle.

* §6 Pattern B note 'the metadata write is lifecycle-snapshotted'
  used a term-of-art without forward reference; clarified that the
  framework snapshots at lifecycle boundaries (defined in §4).

Verification
------------

* tests/durable/test_dev_guide_review.py:        7/7 pass
* tests/durable/test_public_api_surface.py:     12/12 pass
* tests/durable/test_contract_completeness.py:  11/11 pass
* full tests/durable/ suite: 292 pass / 0 fail / 1 unrelated warning

Branch: feature/agentserver-durable-tasks
Spec: 015-core-task-primitive-and-sample-fixups (Phase 11 follow-up)

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

Removes session_id, retry, and stream_handler from the public signatures of
Task.run() and Task.start(). All three were footguns at the crash boundary:

- session_id is platform-derived from FOUNDRY_AGENT_SESSION_ID; handler code
  has no business overriding it.
- retry (RetryPolicy) and stream_handler are needed when crash recovery
  re-invokes the task on a fresh process. The manager's _recover_stale_tasks
  only sees the decorator-level opts (self._resume_opts.get(fn_name)), so any
  per-call override would silently disappear at recovery — the recovered
  lifetime would use the decorator config, not the per-call config. Removing
  them from the public surface forces the durable-safe shape: configure on
  @task (or Task.options) so the value survives across lifetimes.

Also renames TaskContext.session_id to _session_id. It was framework-internal
plumbing not intended for handler consumption; no external readers exist.

Internal _lifecycle_start keeps the session_id/retry/stream_handler params
because the manager threads platform-derived values through that boundary.

Test changes (no semantic regressions):

- tests/durable/test_stream_handler.py: 6 tests migrated from per-call
  stream_handler= to @task(stream_handler_factory=lambda _tid: handler).
  test_call_site_handler_overrides_factory deleted — it specifically
  validated the override behavior being removed.
- tests/durable/test_retry.py: 5 tests migrated from per-call retry= to
  @task(retry=...).

Verified:
- core: 291/291 durable tests pass
- responses: 1300/1301 non-live pass (one timing flake on
  test_shutdown_durable_background_not_marked_failed; passes in isolation)
- responses durability_contract suite: 37/37 pass
- invocations: 245/245 non-live pass

CHANGELOG entry added under existing 2.0.0b4 section. Dev guide section 5
already updated in prior commit to reflect the trimmed signatures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…; add input_id parity on .run()

Round 2 of the post-spec-015 API simplification on Task.run / Task.start
per developer feedback on docs/durable-task-guide.md.

* Drop `title`, `tags`, `stale_timeout` from `Task.run()` and
  `Task.start()`. All three are recovery-safe characteristics of the
  task identity and must survive crash/recovery; per-call overrides
  would silently disappear when recovery re-invokes the task on a fresh
  process. `title`/`tags` are configured on `@task(title=..., tags=...)`
  (both accept callable factories); `stale_timeout` is now a decorator
  option on `@task(stale_timeout=...)` and `Task.options(stale_timeout=...)`.
  Default unchanged at 300 seconds.
* Add `input_id` and `if_last_input_id` to `Task.run()` (parity with
  `Task.start()`). Allows fire-and-await callers to use the optimistic-
  concurrency precondition without a separate .start() + .result() pair.
* `TaskOptions` gains a `stale_timeout: float = 300.0` slot; propagated
  through `_lifecycle_start` from `self._opts`.
* Doc updates in `durable-task-guide.md`: §5 `Task` signature trimmed
  to only the legitimately per-call kwargs; `@task` reference table
  enumerates every decorator option including the new `stale_timeout`;
  new §7 subsection 'Stale-task recovery and `stale_timeout`' explains
  what it does, what it interacts with (background lease-reclaim is
  separate), and when to override.
* Delete the two legacy redirect docs (`durable-task-overview.md` and
  `durable-task-developer-guide.md`); update all in-tree references
  (README, contract-completeness error message, responses-orchestrator
  comment) to point at `durable-task-guide.md`.
* Migrate 6 durable test files off the dropped per-call kwargs:
  promote to `@task(stale_timeout=...)` where the value is constant;
  use `Task.options(stale_timeout=...)` when a single test needs two
  variants on the same handler. Two tests covering the per-call
  `tags=` merge / reserved-stripping surface are deleted — the
  decorator-level coverage for both behaviours remains.

Tests:
- tests/durable/: 289 passed (down from 291; 2 tests covered a now-
  removed surface).
- tests/durable/test_dev_guide_review.py + test_contract_completeness.py:
  27 passed.
- responses tests/e2e/durability_contract/: 37 passed.
- invocations tests/: 245 passed.

Pre-release packages (2.0.0b4); CHANGELOG appended in place.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Naman Tyagi and others added 2 commits July 23, 2026 10:17
# Conflicts:
#	sdk/agentserver/azure-ai-agentserver-core/CHANGELOG.md
#	sdk/agentserver/azure-ai-agentserver-invocations/CHANGELOG.md
#	sdk/agentserver/azure-ai-agentserver-invocations/pyproject.toml
#	sdk/agentserver/azure-ai-agentserver-responses/CHANGELOG.md
Addresses Copilot review feedback: replace deprecated asyncio.get_event_loop()
with asyncio.get_running_loop() in the resilient-task manager/lease code paths,
all of which execute inside a running event loop.

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 23, 2026 05:10

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

This comment has been minimized.

Naman Tyagi and others added 2 commits July 23, 2026 12:15
…ken tests

Samples:
- resilient_multiturn poll endpoint now reads per-invocation state from the
  nested payload["metadata"] slot (where the default metadata namespace is
  persisted) instead of the payload top level, which always returned 404.
- resilient_research cancel handler now resolves the active run via the
  manager's task_id-only accessor instead of MultiTurnTask.get_active_run,
  which requires the in-flight turn's input_id and raised TypeError (500).

Tests:
- test_etag_cas: seed the stale task with schema_version so it is reclaimed
  (not legacy-deleted) and assert the reclaim issued a PATCH, so the if_match
  CAS assertion is no longer vacuous.
- test_lease_renewal: shrink the lease so the renewal loop actually ticks
  within the test window, so the dynamic-cadence shadow assertion exercises
  the shadow logic instead of passing by construction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5222ab61-7d5e-4642-b75f-ef365e375f4f
Mirror the existing POSIX-only skip convention (not hasattr(os, "fork"))
for tests that cannot run on Windows:

- test_contract_completeness: module-level skip — its helpers scan repo
  source files with the platform default encoding, and cp1252 on Windows
  cannot decode UTF-8 source bytes (e.g. the 0x90 byte in test_lifecycle.py).
- test_us4_support: skip only the grep-based regression guard, which shells
  out to the POSIX `grep` binary (absent on Windows). The other tests still
  run everywhere.
- test_resilient_langgraph / test_resilient_research_live: skip on non-POSIX
  because CrashHarness drives crash recovery via POSIX process-group signals
  (os.killpg / os.getpgid / start_new_session).

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 23, 2026 06:49

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

This comment has been minimized.

…d CI failures

Pre-existing PR CI failures (not from the sample/test fixes):

- Verify Links: docs/streaming-guide.md linked to `./tasks-guide.md` with a
  relative path; switched to the full GitHub URL (the link checker rewrites
  blob/main -> PR SHA), matching the CHANGELOG convention.
- Build Analyze (cspell): added the technical words configurators, CREAT,
  impls, RDWR, unimportable to the core package cspell ignore list.
- Build Docs (Sphinx): fixed six reST docstring warnings (warnings are errors)
  in _base.AgentServerHost.register_pre_shutdown_callback,
  streaming/_protocol.EventStreamNotFoundError, the tasks package docstring,
  tasks/_context.TaskContext.exit_for_recovery and tasks/_decorator.multi_turn_task
  (stray indents, an unterminated inline literal, an orphaned period, and an
  under-indented field continuation). Validated clean with docutils.
- consistency: regenerated api.md / api.metadata.yml for azure-ai-agentserver-core
  with apiview-stub-generator 0.3.30 (the committed api.md rendered a stale,
  short @DataClass signature).

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 23, 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

The Azure Pipelines build has three failures across two packages:

Package Check Result Detail
azure-ai-agentserver-core verifysdist ❌ FAIL samples folder is present in source but missing from the built sdist
azure-ai-agentserver-core pylint ❌ FAIL(16) azure/ai/agentserver/core/__init__.py:4 — line too long (136/120)
azure-ai-agentserver-invocations pylint ❌ FAIL(16) azure/ai/agentserver/invocations/__init__.py:4 — line too long (164/120)

All other packages (azure-ai-agentserver-activity, -optimization, -responses) passed both checks.

Recommended next steps

  • Fix verifysdist for azure-ai-agentserver-core: The samples/ directory exists in source but is excluded from the sdist. Add samples to MANIFEST.in (e.g. recursive-include samples *) or include it in the packages/package_data configuration in pyproject.toml / setup.cfg so it is bundled in the source distribution.
  • Fix pylint line-too-long in azure-ai-agentserver-core: Shorten or wrap line 4 of sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/__init__.py to ≤ 120 characters.
  • Fix pylint line-too-long in azure-ai-agentserver-invocations: Shorten or wrap line 4 of sdk/agentserver/azure-ai-agentserver-invocations/azure/ai/agentserver/invocations/__init__.py to ≤ 120 characters.
  • 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)
verifysdist summary:
  azure-ai-agentserver-core     FAIL(1) - Source folder [samples] is not included in sdist
  azure-ai-agentserver-invocations   OK
  azure-ai-agentserver-optimization  OK
  azure-ai-agentserver-responses     OK
  azure-ai-agentserver-activity      OK

pylint summary:
  azure-ai-agentserver-core     FAIL(16)
    azure/ai/agentserver/core/__init__.py:4: [C0301(line-too-long)] Line too long (136/120)
  azure-ai-agentserver-invocations   FAIL(16)
    azure/ai/agentserver/invocations/__init__.py:4: [C0301(line-too-long)] Line too long (164/120)
  azure-ai-agentserver-activity      OK
  azure-ai-agentserver-optimization  OK
  azure-ai-agentserver-responses     OK

Pipeline: https://dev.azure.com/azure-sdk/public/_build/results?buildId=6606756

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 · 54 AIC · ⌖ 6.3 AIC · ⊞ 6.7K ·

Pre-existing Build Analyze failures on the PR (independent of the sample/test
and docs fixes):

- verifysdist :: azure-ai-agentserver-core failed with "Source folder [samples]
  is not included in sdist". Core ships samples/state_store_sample.py but its
  MANIFEST.in lacked the samples include; added
  `recursive-include samples *.py *.md` (mirrors the invocations package, whose
  verifysdist passes). Verified locally: verifysdist now exits 0.

- pylint :: core and :: invocations failed with C0301 (line-too-long) on the
  single-line module docstring at __init__.py:4 (136/120 and 164/120). The
  docstrings must stay single-line (apistub mis-parses multi-line package
  __init__ docstrings), so shortened the prose to fit within 120 columns
  (116 and 99 chars). Both still render cleanly (docutils-validated) and do not
  change api.md.

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 23, 2026 09: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.

Copilot AI review requested due to automatic review settings July 24, 2026 04:12

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.

Comment thread sdk/agentserver/azure-ai-agentserver-core/README.md Outdated
Comment thread sdk/agentserver/azure-ai-agentserver-core/README.md Outdated
Reviewer feedback on PR #46997:

- README quickstart used the bare ``@task`` form and ``result.output``, both
  wrong: ``@task`` requires an explicit ``name=`` (validated in
  ``_validate_task_name``), and ``Task.run`` returns the handler's output
  directly (typed ``Output``), not a wrapper. Fixed to ``@task(name=...)`` and
  ``print(result)``. Applied the same ``@task`` -> ``@task(name=...)`` fix to the
  two examples in docs/streaming-guide.md.
- MultiTurnTask.run was annotated ``-> Any``; tightened to ``-> Output`` to match
  Task.run (pyright clean).
- Removed the unused ``from __future__ import annotations`` from
  streaming/__init__.py (no annotations in that module).

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 27, 2026 12:25

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.

…rn type

The MultiTurnTask.run return-type change (Any -> Output) altered the generated
API surface; regenerated api.md / api.metadata.yml with apiview-stub-generator
0.3.30 so the consistency check passes.

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 27, 2026 12:52

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
Nathandrake229 merged commit bee17d1 into main Jul 28, 2026
27 checks passed
@Nathandrake229
Nathandrake229 deleted the feature/agentserver-durable-tasks branch July 28, 2026 05:24
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.

6 participants