[agentserver] Enable resilient tasks in invocations LRA samples + add minimal hello samples - #48878
[agentserver] Enable resilient tasks in invocations LRA samples + add minimal hello samples#48878Nathandrake229 wants to merge 17 commits into
Conversation
…les + add minimal hello samples The durable-task subsystem is strictly opt-in as of azure-ai-agentserver-core 2.2.0b1: AgentServerHost only constructs the TaskManager when set_resilient_tasks_enabled(True) is called before host startup. Without it, <task>.start() raises TaskManagerNotInitialized and there is no crash recovery. The resilient invocations samples declared durable tasks but never enabled the subsystem, so as shipped they would not run as long-running agents. Changes: - resilient_research / resilient_multiturn / resilient_langgraph: call set_resilient_tasks_enabled(True) at import time (before host startup). - Add resilient_hello_world: the smallest start-and-poll LRA (core + invocations only, no LLM), demonstrating durable checkpointing + crash recovery. - Add resilient_hello_forever: a minimal indefinite (while True) durable worker with graceful-shutdown handling and cancel. Verified locally against core 2.2.0b1: all five samples import with the durable subsystem enabled; hello_world and hello_forever resume from their checkpoint after a hard process kill; hello_forever stops on cancel; resilient_research streams end-to-end. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8abde5e2-e955-42bc-957b-7bf6877cd753
|
Azure Pipelines: Successfully started running 1 pipeline(s). 10 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
🟡 Changes recommended
Invocation identity, polling, post-timeout cancellation, and lifecycle test coverage have unresolved moderate issues.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Enables resilient task hosting in existing invocation samples and adds minimal finite and indefinite durable-worker examples.
Changes:
- Enables resilient tasks before host startup in three samples.
- Adds checkpointed start/poll and start/poll/cancel examples.
- Documents recovery behavior and minimal dependencies.
File summaries
| File | Description |
|---|---|
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_research/app.py |
Enables resilient tasks. |
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_multiturn/app.py |
Enables resilient tasks. |
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_langgraph/app.py |
Enables resilient tasks. |
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/requirements.txt |
Declares minimal dependencies. |
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/README.md |
Documents finite resilient execution. |
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/app.py |
Implements start and polling handlers. |
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/agent.py |
Implements checkpointed finite work. |
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/__init__.py |
Initializes the sample package. |
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/requirements.txt |
Declares minimal dependencies. |
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/README.md |
Documents indefinite resilient execution. |
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py |
Implements start, polling, and cancellation handlers. |
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/agent.py |
Implements the checkpointed worker. |
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/__init__.py |
Initializes the sample package. |
Review details
Suppressed comments (12)
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/README.md:26
- This timeout description does not match the task implementation or the cooperative watchdog contract. Expiry only sets
ctx.timeout_exceededandctx.cancel; the loop deliberately continues and therefore is not re-entered. Reserve recovery here for process crashes and graceful redeploys.
It also sets `timeout=timedelta(days=7)` (the maximum per-turn budget), because
each *turn* is watchdog-bounded (default 1 day). When any interruption occurs —
crash, redeploy, or turn-budget expiry — the task is re-entered with
`ctx.entry_mode == "recovered"` and resumes from its checkpointed iteration.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/README.md:61
- Strict opt-in was introduced in core 2.1.0b1 and is present in stable 2.1.0, not first in 2.2.0b1. Update this version reference.
As of `azure-ai-agentserver-core` **2.2.0b1** the durable-task subsystem is
**strictly opt-in**. Before host startup:
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/agent.py:85
- This description of timeout behavior is incorrect. The core watchdog is cooperative-only: it sets
timeout_exceededandcancelbut neither interrupts the handler nor releases its lease, so this branch simply ignores the signal and keeps executing; no recovered re-entry occurs at seven days. Update this comment and the README/module documentation to state that timeout is intentionally ignored, or implement a supported turn-boundary strategy.
# A cancel sets ``ctx.cancel`` without ``timeout_exceeded``; the
# per-turn watchdog also sets ``ctx.cancel`` but WITH
# ``timeout_exceeded`` — in that case we do NOT stop, so the
# framework re-enters (recovered) and the loop keeps going.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py:50
- The core CHANGELOG places strict opt-in in 2.1.0b1, and stable 2.1.0 includes it. Correct the stated version boundary.
# Resilient tasks (durable execution + crash recovery) are strictly opt-in as of
# azure-ai-agentserver-core 2.2.0b1: ``AgentServerHost`` builds the
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/README.md:49
- The implementation does not use task metadata or call
ctx.metadata.flush(); it writes each checkpoint withFoundryStateStore.set_item(). As written, this explanation directs readers to an API that is absent from the sample.
2. The task loops, sleeping between steps and calling `ctx.metadata.flush()`
after each — a **durable checkpoint**.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/README.md:73
- Recovery reads
completed_stepsfrom the explicitFoundryStateStore, not fromctx.metadata. Keep the recovery walkthrough aligned withagent.pyso users know where their checkpoint belongs.
reads `completed_steps` from `ctx.metadata`, and continues from the next
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/README.md:75
- The checkpoint remains after task completion, so the polling handler returns
completedrather than 404. This second recovery walkthrough should not promise a response that the code cannot produce.
`completed_steps` climb past where it crashed (then `404` when it finishes).
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/README.md:56
- This behavior dates to core 2.1.0b1 and is already in stable 2.1.0, per the core CHANGELOG. Replace 2.2.0b1 so the sample states the correct minimum behavior boundary.
As of `azure-ai-agentserver-core` **2.2.0b1** the durable-task subsystem is
**strictly opt-in**. `AgentServerHost` builds the `TaskManager` (and runs the
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/app.py:47
- Core's CHANGELOG records this opt-in change under 2.1.0b1, and stable 2.1.0 already contains it. Correct the version boundary here.
# Resilient tasks (durable execution + crash recovery) are strictly opt-in as of
# azure-ai-agentserver-core 2.2.0b1: ``AgentServerHost`` builds the
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_langgraph/app.py:86
- The strict opt-in behavior was introduced in core 2.1.0b1 and is included in stable 2.1.0, not first introduced in 2.2.0b1. Correct this compatibility statement.
# Resilient tasks (durable execution + crash recovery) are strictly opt-in as of
# azure-ai-agentserver-core 2.2.0b1: ``AgentServerHost`` constructs the
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_multiturn/app.py:49
- The strict opt-in behavior was introduced in core 2.1.0b1 and is present in stable 2.1.0, according to the core CHANGELOG. Replace 2.2.0b1 here to avoid documenting the wrong compatibility boundary.
# Resilient tasks (durable execution + crash recovery) are strictly opt-in as of
# azure-ai-agentserver-core 2.2.0b1: ``AgentServerHost`` constructs the
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_research/app.py:104
- The core CHANGELOG places the strict opt-in breaking change in 2.1.0b1 (and it is included in stable 2.1.0), not 2.2.0b1. Correct the version so users do not infer that this behavior requires an unreleased 2.2 build.
# Resilient tasks (durable execution + crash recovery) are strictly opt-in as of
# azure-ai-agentserver-core 2.2.0b1: ``AgentServerHost`` constructs the
- Files reviewed: 11/13 changed files
- Comments generated: 7
- Review effort level: Balanced
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
- Key both samples by the platform invocation id (request.state.invocation_id)
instead of a sample-only ?task_id= query param, so standard clients poll/cancel
via GET/POST /invocations/{invocation_id}. Updated docstrings and READMEs.
- hello_world GET: when no checkpoint exists yet, consult get_active_run() and
return in_progress/0 while the run is active instead of an immediate 404 right
after POST.
- hello_world README: the durable checkpoint persists after the ephemeral task
record is cleaned up, so a completed run polls as "completed" (removed the
incorrect 404-on-completion contract and a stale ctx.metadata reference).
- hello_forever cancel robustness: replace the timeout_exceeded-based stop
condition (which could not stop the worker after the per-turn watchdog fired,
and where the cancel path does not set cancel_requested) with a durable stop
marker written by the cancel endpoint and checked by the worker — robust across
the per-turn timeout and across recovery.
Re-verified locally against core 2.2.0b1: hello_world polls in_progress
immediately after POST and ends completed; hello_forever stops on cancel.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8abde5e2-e955-42bc-957b-7bf6877cd753
|
Thanks for the review — pushed d88c1b9 addressing the inline comments: Invocation-id keying (app.py, both samples) — both now key the durable task by hello_world 404 right after POST — hello_world README 404-on-completion — corrected. The durable checkpoint persists after the ephemeral hello_forever cancel after the timeout watchdog — good catch. The cancel path ( Automated coverage (agent.py comments) — these two samples are currently validated manually against core 2.2.0b1 (start → checkpoint poll → hard-kill recovery → completion for hello_world; start → cancel → stop, and crash → resume for hello_forever). Adding contract/E2E coverage parallel to the existing resilient samples is a reasonable follow-up; happy to add it in this PR if you'd prefer it here rather than a follow-up. |
There was a problem hiding this comment.
🟡 Changes recommended
One or more issues must be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (11)
Previously missed (3) — in code that hasn't changed since the last review.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py:67
- A syntactically valid JSON scalar or array reaches
data.getand raisesAttributeError, producing a 500 response. Reject non-object bodies with a 400 response before readingname.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/app.py:66 - A valid non-object JSON body makes
data.getraise, a nonnumericstepsvalue makesintraise, andsteps <= 0completes without ever writing a checkpoint so subsequent polling returns 404. Validate that the body is an object and thatstepsis positive, returning a client error instead of a server error or an unpollable run.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/agent.py:21 - This claims turn-budget expiry re-enters the task, but the watchdog is cooperative and this handler keeps looping after
ctx.timeout_exceededis set. Correct the module documentation so it does not promise recovery behavior the implementation cannot provide.
This issue also appears on line 95 of the same file.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/README.md:26
- The timeout watchdog is cooperative: it only sets
ctx.timeout_exceededandctx.cancel; it never interrupts or re-enters the handler. This loop deliberately ignores a timeout without a stop marker, so the documented turn-budget-expiry recovery does not occur. Describe the actual behavior instead.
It also sets `timeout=timedelta(days=7)` (the maximum per-turn budget), because
each *turn* is watchdog-bounded (default 1 day). When any interruption occurs —
crash, redeploy, or turn-budget expiry — the task is re-entered with
`ctx.entry_mode == "recovered"` and resumes from its checkpointed iteration.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/agent.py:98
- The durable stop marker is only read after this process-local
ctx.cancelevent is set. If cancellation reaches another replica (whereget_active_run()returnsNone) or the process dies after writing the marker but before signaling the run, recovery starts with a fresh unset event and this worker ignores the persisted stop forever. Check the marker independently on every iteration.
if ctx.cancel.is_set():
if await store.get_item(stop_key) is not None:
logger.info("stop requested — stopping at iteration %d", n)
return {"name": name, "iterations": n, "stopped": True}
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/agent.py:61
- Existing resilient invocation samples have structural and end-to-end tests, but this new infinite worker has no automated recovery or cancellation coverage. Add it to the structural gate and test both crash resume and cancellation, especially cancellation from a process that does not own the active run.
@task(name="hello_forever", timeout=timedelta(days=7))
async def hello_forever(ctx: TaskContext[dict]) -> dict[str, Any]:
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/README.md:73
- Ctrl-C initiates graceful host shutdown, so this procedure does not exercise the hard-crash recovery path described by the sample and PR. Use an actual hard kill so the verification matches the claimed scenario.
2. Poll until `completed_steps` is a few in, then **kill the process** (Ctrl-C).
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/agent.py:45
- The repository has a structural gate plus end-to-end recovery tests for every existing resilient invocation sample, but this new sample is absent from both. Add it to
test_resilient_samples_structure.pyand cover checkpoint continuation through the crash harness so the core behavior demonstrated here cannot regress.
@task(name="hello_world")
async def hello_world(ctx: TaskContext[dict]) -> dict[str, Any]:
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_langgraph/app.py:91
- These statements are contradictory: when
langgraph_session.start()raisesTaskManagerNotInitialized, the agent does not continue in a non-durable mode. Clarify that execution cannot start and crash recovery is unavailable.
# startup (i.e. at module-import time). Without it, ``get_task_manager()`` /
# ``langgraph_session.start()`` raise ``TaskManagerNotInitialized`` and the agent
# runs non-durably with no crash recovery — defeating the purpose of a resilient
# long-running agent.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_multiturn/app.py:54
- These statements are contradictory: when
session_workflow.start()raisesTaskManagerNotInitialized, the agent does not continue in a non-durable mode. Clarify that execution cannot start and crash recovery is unavailable.
# startup (i.e. at module-import time). Without it, ``get_task_manager()`` /
# ``session_workflow.start()`` raise ``TaskManagerNotInitialized`` and the agent
# runs non-durably with no crash recovery — defeating the purpose of a resilient
# long-running agent.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_research/app.py:109
- These statements are contradictory: when
deep_research.start()raisesTaskManagerNotInitialized, the agent does not continue in a non-durable mode. Clarify that execution cannot start and crash recovery is unavailable.
# startup (i.e. at module-import time). Without it, ``get_task_manager()`` /
# ``deep_research.start()`` raise ``TaskManagerNotInitialized`` and the agent
# runs non-durably with no crash recovery — defeating the purpose of a resilient
# long-running agent.
- Files reviewed: 11/13 changed files
- Comments generated: 1
- Review effort level: Balanced
Extend the invocations sample test suite to cover the two new minimal long-running-agent samples and guard the opt-in flag this PR adds. Structure tests (test_resilient_samples_structure.py): - Register resilient_hello_world and resilient_hello_forever: directory existence, required files (agent.py, app.py, requirements.txt, README.md), and no-retired-name references. - Add a regression gate asserting every resilient sample's app.py calls set_resilient_tasks_enabled(True). Durable tasks are strictly opt-in since core 2.1.0b1; without the call get_task_manager() raises TaskManagerNotInitialized and recovery is silently disabled. E2E tests (in-process, no LLM / cloud, cross-platform): - test_resilient_hello_world.py: run-to-completion checkpoints every step; a fully-completed checkpoint skips all work (unchanged ETag); a partial checkpoint resumes at the next step. - test_resilient_hello_forever.py: the worker ticks and checkpoints, then an explicit cancel plus durable stop marker stops it terminally; a pre-existing checkpoint makes it resume past the seeded cursor instead of restarting. These mirror the existing test_resilient_multiturn.py in-process pattern (real TaskManager over the local file provider at tmp_path) and touch only tests/ — no shipped SDK source changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8abde5e2-e955-42bc-957b-7bf6877cd753
Addresses PR review: the GET handler reported `stopped` whenever `get_active_run()` returned None on the polled replica. For an indefinite worker that keeps running (possibly on a different replica) until explicitly cancelled, process-local ownership is not a reliable terminal signal. Derive status from the durable stop marker instead, so every replica agrees: present => stopped, absent => running. 404 only when neither checkpoint nor marker exists. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8abde5e2-e955-42bc-957b-7bf6877cd753
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate issues affect durable cancellation, input validation, dependency compatibility, and regression coverage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (15)
Previously missed (13) — in code that hasn't changed since the last review.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/agent.py:101
- The durable marker is only read while this process's in-memory cancel event is set. If the process crashes after the endpoint writes the marker but before
run.cancel()is delivered, recovery creates a fresh context withctx.cancelunset and the worker ignores the persisted stop forever. Check the marker independently on every iteration so it actually survives crash/recovery as advertised.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py:67 - A syntactically valid JSON body can be a list, string, or null, in which case
data.get(...)raisesAttributeErrorand the sample returns a 500. Validate that the decoded body is an object and return a 400 for unsupported shapes.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/requirements.txt:8 - These unbounded requirements can silently resolve to incompatible future major versions. The package itself constrains core to
>=2.1.0,<3.0.0(pyproject.toml:24), and shipped invocation samples constrain invocations to<2.0.0(for examplesamples/simple_invoke_agent/requirements.txt:1); apply the same compatibility bounds here.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/app.py:66 stepsaccepts zero or negative values, so the task completes without ever writing a checkpoint. Because one-shot task records are deleted on completion, the documented poll endpoint then returns 404 for a run that successfully returned 202. Reject non-positive (and non-object/non-integer) input with 400 before starting the task.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/requirements.txt:8- These unbounded requirements can silently resolve to incompatible future major versions. The package itself constrains core to
>=2.1.0,<3.0.0(pyproject.toml:24), and shipped invocation samples constrain invocations to<2.0.0(for examplesamples/simple_invoke_agent/requirements.txt:1); apply the same compatibility bounds here.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/README.md:26 - The timeout watchdog does not interrupt or re-enter this handler. It only sets cooperative context flags, which the current loop ignores when no stop marker exists, so turn-budget expiry does not cause the recovery described here.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/README.md:60 - The opt-in behavior was introduced in core 2.1.0b1, not 2.2.0b1 (see
azure-ai-agentserver-core/CHANGELOG.md:28-44). The README should identify the first affected release correctly.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/agent.py:21 - Core does not interrupt or re-enter a task when this timeout expires; its watchdog only sets cooperative flags, and this loop continues when no stop marker exists. Rewrite this explanation unless the handler is changed to implement an actual rollover path.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py:50 - The opt-in behavior was introduced in core 2.1.0b1, not 2.2.0b1 (see
azure-ai-agentserver-core/CHANGELOG.md:28-44). Keeping the wrong version here makes the sample's compatibility guidance inaccurate.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/README.md:58 - The opt-in behavior was introduced in core 2.1.0b1, not 2.2.0b1 (see
azure-ai-agentserver-core/CHANGELOG.md:28-44). The README should identify the first affected release correctly.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/app.py:48 - The opt-in behavior was introduced in core 2.1.0b1, not 2.2.0b1 (see
azure-ai-agentserver-core/CHANGELOG.md:28-44). Keeping the wrong version here makes the sample's compatibility guidance inaccurate.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_multiturn/app.py:49 - The opt-in behavior was introduced in core 2.1.0b1, not 2.2.0b1 (see
azure-ai-agentserver-core/CHANGELOG.md:28-44). Keeping the wrong version here misleads users about which released core versions require this call.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_research/app.py:104 - The opt-in behavior was introduced in core 2.1.0b1, not 2.2.0b1 (see
azure-ai-agentserver-core/CHANGELOG.md:28-44). Keeping the wrong version here misleads users about which released core versions require this call.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_langgraph/app.py:86
- The opt-in behavior was introduced in core 2.1.0b1, not 2.2.0b1 (see
azure-ai-agentserver-core/CHANGELOG.md:28-44). Keeping the wrong version here misleads users about which released core versions require this call.
# azure-ai-agentserver-core 2.2.0b1: ``AgentServerHost`` constructs the
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_langgraph/app.py:89
- Core's CHANGELOG records the strict opt-in under 2.1.0b1 (
azure-ai-agentserver-core/CHANGELOG.md:28-44), not 2.2.0b1. Also, without the switchlanggraph_session.start()raises before work begins; it does not run non-durably. Correct both claims so the sample accurately identifies affected users and failure behavior.
# Resilient tasks (durable execution + crash recovery) are strictly opt-in as of
# azure-ai-agentserver-core 2.2.0b1: ``AgentServerHost`` constructs the
# ``TaskManager`` ONLY when this switch is on, and it must be set before host
# startup (i.e. at module-import time). Without it, ``get_task_manager()`` /
# ``langgraph_session.start()`` raise ``TaskManagerNotInitialized`` and the agent
- Files reviewed: 14/16 changed files
- Comments generated: 1
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
A critical cross-replica cancellation defect and unresolved request-validation and dependency issues remain.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (16)
Previously missed (6) — in code that hasn't changed since the last review.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py:67
- A valid non-object JSON body such as
[]ornullreachesdata.getand becomes a 500 response. Validate that the decoded body is an object and return 400 for unsupported shapes.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/requirements.txt:8 - These unconstrained requirements can be satisfied by an older installed package that lacks the local state-store/task behavior this no-cloud sample needs. Use the package's supported release ranges, consistent with
pyproject.toml:24and the bounded requirements insamples/simple_invoke_agent/requirements.txt:1.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/app.py:66 - Invalid request bodies are either silently accepted as defaults (malformed JSON) or turn into 500s (
[], non-numericsteps), and non-positivestepsproduce no checkpoint so later polls return 404. Validate the JSON object and positive integer before starting the task, returning 400 for invalid input.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/requirements.txt:8 - These unconstrained requirements can be satisfied by an older installed package that lacks the local state-store/task behavior this no-cloud sample needs. Use the package's supported release ranges, consistent with
pyproject.toml:24and the bounded requirements insamples/simple_invoke_agent/requirements.txt:1.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/README.md:26 - Turn-budget expiry does not cause re-entry: the watchdog is cooperative-only and this handler deliberately continues after its signal. This walkthrough therefore promises a recovery mode that users will not observe.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/agent.py:21 - Core's watchdog is cooperative-only and explicitly does not end or re-enter an ignoring handler (
tasks/_manager.py:1613-1616). This description incorrectly promises recovery at turn-budget expiry; the current loop simply continues in the same turn after the signal.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/README.md:60
- The opt-in breaking change shipped in core 2.1.0b1, not 2.2.0b1 (
azure-ai-agentserver-core/CHANGELOG.md:28-40). Stating the later version can mislead users of 2.1.x into omitting the required switch.
As of `azure-ai-agentserver-core` **2.2.0b1** the durable-task subsystem is
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py:50
- The opt-in breaking change shipped in core 2.1.0b1, not 2.2.0b1 (
azure-ai-agentserver-core/CHANGELOG.md:28-40). Stating the later version can mislead users of 2.1.x into omitting the required switch.
# azure-ai-agentserver-core 2.2.0b1: ``AgentServerHost`` builds the
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/README.md:58
- The opt-in breaking change shipped in core 2.1.0b1, not 2.2.0b1 (
azure-ai-agentserver-core/CHANGELOG.md:28-40). Stating the later version can mislead users of 2.1.x into omitting the required switch.
As of `azure-ai-agentserver-core` **2.2.0b1** the durable-task subsystem is
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/app.py:48
- The opt-in breaking change shipped in core 2.1.0b1, not 2.2.0b1 (
azure-ai-agentserver-core/CHANGELOG.md:28-40). Stating the later version can mislead users of 2.1.x into omitting the required switch.
# azure-ai-agentserver-core 2.2.0b1: ``AgentServerHost`` builds the
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_langgraph/app.py:86
- The opt-in breaking change shipped in core 2.1.0b1, not 2.2.0b1 (
azure-ai-agentserver-core/CHANGELOG.md:28-40). Stating the later version can mislead users of 2.1.x into omitting the required switch.
# azure-ai-agentserver-core 2.2.0b1: ``AgentServerHost`` constructs the
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_langgraph/app.py:91
- A direct
@task.start()call does not degrade to non-durable execution when the manager is disabled; it raises and the task does not run, as core's contract states. Remove the claim that this agent still runs non-durably.
# ``langgraph_session.start()`` raise ``TaskManagerNotInitialized`` and the agent
# runs non-durably with no crash recovery — defeating the purpose of a resilient
# long-running agent.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_multiturn/app.py:49
- The opt-in breaking change shipped in core 2.1.0b1, not 2.2.0b1 (
azure-ai-agentserver-core/CHANGELOG.md:28-40). Stating the later version can mislead users of 2.1.x into omitting the required switch.
# azure-ai-agentserver-core 2.2.0b1: ``AgentServerHost`` constructs the
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_multiturn/app.py:54
- A direct
@task.start()call does not degrade to non-durable execution when the manager is disabled; it raises and the task does not run, as core's contract states. Remove the claim that this agent still runs non-durably.
# ``session_workflow.start()`` raise ``TaskManagerNotInitialized`` and the agent
# runs non-durably with no crash recovery — defeating the purpose of a resilient
# long-running agent.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_research/app.py:104
- The opt-in breaking change shipped in core 2.1.0b1, not 2.2.0b1 (
azure-ai-agentserver-core/CHANGELOG.md:28-40). Stating the later version can mislead users of 2.1.x into omitting the required switch.
# azure-ai-agentserver-core 2.2.0b1: ``AgentServerHost`` constructs the
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_research/app.py:109
- A direct
@task.start()call does not degrade to non-durable execution when the manager is disabled; it raises and the task does not run, as core's contract states. Remove the claim that this agent still runs non-durably.
# ``deep_research.start()`` raise ``TaskManagerNotInitialized`` and the agent
# runs non-durably with no crash recovery — defeating the purpose of a resilient
# long-running agent.
- Files reviewed: 14/16 changed files
- Comments generated: 1
- Review effort level: Balanced
…dent Addresses two PR review comments: - test_resilient_samples_structure.py: the resilient-tasks opt-in guard matched a substring, which a comment or a late call would satisfy. Parse the module AST instead and assert a real set_resilient_tasks_enabled(True) call precedes InvocationAgentServerHost() construction (the switch is read at host build time, so a later call is too late). - resilient_hello_forever/agent.py: the worker only read the durable stop marker when ctx.cancel was set. A cancel routed to a different replica writes the marker but cannot set the owning process's ctx.cancel, so that worker would run forever while polling reported 'stopped'. Check the marker every iteration independently of ctx.cancel (unconditional tick sleep avoids a busy loop after the per-turn watchdog sets ctx.cancel). Updated the sample docstring and added an e2e test proving the marker alone stops the worker. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8abde5e2-e955-42bc-957b-7bf6877cd753
There was a problem hiding this comment.
🟡 Changes recommended
Critical checkpoint isolation and multiple correctness, dependency, and test issues remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (10)
Previously missed (6) — in code that hasn't changed since the last review.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py:102
- A successful POST can still be followed by a transient 404:
start()schedules the worker, but the worker awaits store creation/read before writing its first checkpoint, so this branch can run before either item exists. This is especially visible when polling is routed to another replica. Persist an initial durableiterations: 0record as part of starting the invocation so every replica can distinguish a just-started worker from an unknown ID.
This issue also appears in the following locations of the same file:
- line 107
- line 124
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/requirements.txt:8
- These unbounded requirements can resolve to a future incompatible major release, contrary to the package's sample dependency convention (
samples/simple_invoke_agent/requirements.txt:1) and the runtime Core bound (pyproject.toml:24). Keep the sample dependency-light while constraining it to the compatible release lines.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/app.py:66 steps <= 0starts successfully but executes no loop iteration, so no checkpoint is ever created; after the ephemeral task record is removed, every poll returns 404 instead ofcompleted. Reject non-positive step counts before starting the task (or persist a completion checkpoint for zero steps).
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/requirements.txt:8- These unbounded requirements can resolve to a future incompatible major release, contrary to the package's sample dependency convention (
samples/simple_invoke_agent/requirements.txt:1) and the runtime Core bound (pyproject.toml:24). Keep the sample dependency-light while constraining it to the compatible release lines.
sdk/agentserver/azure-ai-agentserver-invocations/tests/e2e/test_resilient_hello_forever.py:34 - This collection-time environment mutation is process-global and is never restored, so merely collecting this module can make unrelated tests run as non-hosted. The fixture already removes this variable with
monkeypatch, which restores it after each test; remove the module-level mutation.
sdk/agentserver/azure-ai-agentserver-invocations/tests/e2e/test_resilient_hello_world.py:37 - This collection-time environment mutation is process-global and is never restored, so merely collecting this module can make unrelated tests run as non-hosted. The fixture already removes this variable with
monkeypatch, which restores it after each test; remove the module-level mutation.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/README.md:26
- The timeout claim is incorrect for this implementation. Task timeouts are cooperative: the watchdog only sets
ctx.timeout_exceededandctx.cancel, while this loop ignores both, so budget expiry does not interrupt or re-enter the task. Document that the worker intentionally continues after the watchdog, or add explicit timeout handling consistent with the desired lifecycle.
It also sets `timeout=timedelta(days=7)` (the maximum per-turn budget), because
each *turn* is watchdog-bounded (default 1 day). When any interruption occurs —
crash, redeploy, or turn-budget expiry — the task is re-entered with
`ctx.entry_mode == "recovered"` and resumes from its checkpointed iteration.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/agent.py:52
FoundryStateStoreis agent-scoped by default and explicitly has no built-in session isolation, but this constant shares checkpoints and stop markers across every session. A caller-supplied invocation ID can therefore collide with another session, including making one session's cancel marker stop another worker. Scope the store name by session and persist that session ID in the task input for recovery.
CHECKPOINT_STORE = "hello_forever_checkpoints"
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py:107
- The stop marker records that cancellation was requested, not that the worker has terminated.
handle_cancelwrites it beforerun.cancel(), so an immediate poll reportsstoppedwhile the owner may still be sleeping or executing another iteration. Use separate durablestop_requestedand terminalstoppedstate (the worker should write the latter immediately before returning), and reportcancellinguntil that acknowledgement exists.
"status": "stopped" if stop_marker is not None else "running",
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py:126
- This creates a durable stop marker even when the invocation never existed. Consequently
POST /unknown/cancelreturns success and the subsequent GET reports a fabricatedstoppedinvocation, whereas the invocations contract tests require unknown cancellation to return404. Verify the durable task/checkpoint before writing, and also reject an already-present stop marker.
store = await FoundryStateStore.get_or_create(CHECKPOINT_STORE)
try:
await store.set_item(f"{invocation_id}{STOP_SUFFIX}", {"stop": True})
- Files reviewed: 14/16 changed files
- Comments generated: 3
- Review effort level: Balanced
… docs Addresses three PR review comments: - Session isolation (hello_world + hello_forever): the checkpoint store was a single global name, so another session reusing a caller-supplied invocation id could read or overwrite a run's progress. Namespace the store by the invocation's session id (checkpoint_store_name(session_id)) as the other resilient samples do, and carry session_id in the durable task input so recovery re-enters with the same scope. app.py reads request.state.session_id in start/poll/cancel; READMEs and curl examples pass ?agent_session_id=. - Opt-in structure gate: ast.walk accepted a set_resilient_tasks_enabled(True) call nested in an uncalled def/class. Inspect only direct module-level expression/assignment statements so the guard enforces import-time execution before InvocationAgentServerHost() construction. - hello_forever timeout docs: the per-turn watchdog is cooperative (only sets ctx.timeout_exceeded/ctx.cancel); the loop ignores it, so turn-budget expiry cannot cause re-entry. Corrected the module docstring and README to describe crash/redeploy recovery only. Tests updated to pass session_id and use the session-scoped store name. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8abde5e2-e955-42bc-957b-7bf6877cd753
Addresses this round of PR review comments: - Multi-user isolation (both samples): a single agent session can serve multiple users, but the identity used only session + caller-supplied invocation id, so two users reusing an invocation id shared the TaskManager record, checkpoint, and stop marker. durable_task_id now also incorporates request.state.user_id; the checkpoint store is opened via a new open_checkpoint_store() helper with user_isolation=True + the explicit user id; user_id is carried in the durable input so recovery reopens the same partition. - call-id for recovery (both samples): recovered tasks have no inbound request, and TaskManager restores x-agent-foundry-call-id only from input["call_id"]. Persist request.state.call_id in the durable input; updated the handler-test request stub accordingly. - Non-atomic seed (both samples): the get-then-set initial checkpoint could let concurrent retries clobber an advanced checkpoint. Seed with create_item (atomic create) and treat FoundryStorageConflictError as "already seeded". - Test global-flag leak: importing the app modules flips the process-global set_resilient_tasks_enabled(True); the handler-test fixture now saves and restores resilient_tasks_enabled() so the flag does not leak across tests. Task-level e2e tests updated to open the store via the sample's open_checkpoint_store (session + user) and pass user_id in inputs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8abde5e2-e955-42bc-957b-7bf6877cd753
There was a problem hiding this comment.
🔵 Needs a closer look
Malformed byte bodies can return 500, and the structural test enforces an invalid lifecycle ordering.
Review details
Suppressed comments (12)
Previously missed (10) — in code that hasn't changed since the last review.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py:81
json.loads()raisesUnicodeDecodeErrorfor a non-UTF-8 byte body, so this handler returns 500 for malformed input despite its 400 contract. Catch that exception alongsideJSONDecodeError.
This issue also appears on line 112 of the same file.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/app.py:68
json.loads()raisesUnicodeDecodeErrorfor a non-UTF-8 byte body, so this handler returns 500 for malformed input despite its 400 contract. Catch that exception alongsideJSONDecodeError.
This issue also appears on line 111 of the same file.
sdk/agentserver/azure-ai-agentserver-invocations/tests/test_resilient_samples_structure.py:331
- This ordering requirement does not match the host lifecycle.
AgentServerHostreads the flag inside its ASGI lifespan (core/_base.py:312-350), not duringInvocationAgentServerHost()construction, soapp = InvocationAgentServerHost(); set_resilient_tasks_enabled(True)is valid but this test rejects it. Keep the module-level-call check, but remove the constructor-order assertion and update the surrounding explanation.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/README.md:68 - The opt-in breaking change shipped in core 2.1.0b1, not 2.2.0b1 (
azure-ai-agentserver-core/CHANGELOG.md:28,36-45). Using the later version misleads users of 2.1.0b1 into thinking the switch is unnecessary.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py:66 - The opt-in breaking change shipped in core 2.1.0b1, not 2.2.0b1 (
azure-ai-agentserver-core/CHANGELOG.md:28,36-45). Using the later version misleads users of 2.1.0b1 into thinking the switch is unnecessary.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/README.md:62 - The opt-in breaking change shipped in core 2.1.0b1, not 2.2.0b1 (
azure-ai-agentserver-core/CHANGELOG.md:28,36-45). Using the later version misleads users of 2.1.0b1 into thinking the switch is unnecessary.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/app.py:53 - The opt-in breaking change shipped in core 2.1.0b1, not 2.2.0b1 (
azure-ai-agentserver-core/CHANGELOG.md:28,36-45). Using the later version misleads users of 2.1.0b1 into thinking the switch is unnecessary.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_langgraph/app.py:86 - The opt-in breaking change shipped in core 2.1.0b1, not 2.2.0b1 (
azure-ai-agentserver-core/CHANGELOG.md:28,36-45). Using the later version misleads users of 2.1.0b1 into thinking the switch is unnecessary.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_multiturn/app.py:49 - The opt-in breaking change shipped in core 2.1.0b1, not 2.2.0b1 (
azure-ai-agentserver-core/CHANGELOG.md:28,36-45). Using the later version misleads users of 2.1.0b1 into thinking the switch is unnecessary.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_research/app.py:104 - The opt-in breaking change shipped in core 2.1.0b1, not 2.2.0b1 (
azure-ai-agentserver-core/CHANGELOG.md:28,36-45). Using the later version misleads users of 2.1.0b1 into thinking the switch is unnecessary.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py:114
- After cancellation,
hello_foreverreturns normally, so its one-shot TaskManager record is deleted, but this non-expiring checkpoint and stop marker remain. A retry with the same invocation ID then swallows this conflict, starts a fresh task, returns202, and that task immediately exits on the stale marker. Detect the existing stop marker here and return the already-stopped invocation instead of starting a new worker.
await store.create_item(task_id, {"name": name, "iterations": 0})
except FoundryStorageConflictError:
pass # already seeded by a concurrent request or a previous attempt
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/app.py:115
- A completed one-shot task is deleted by the framework (
core/tasks/_manager.py:2656-2660), while this checkpoint remains. A retry of the same invocation ID after completion therefore reaches this conflict branch, then successfully creates a new task instead of hittingTaskConflictError; it can return202 startedwhile doing no work, or continue the old checkpoint with different input. Treat an already-completed checkpoint as the existing invocation rather than starting another task.
await store.create_item(
task_id, {"name": name, "steps": steps, "completed_steps": 0}
)
except FoundryStorageConflictError:
pass # already seeded by a concurrent request or a previous attempt
- Files reviewed: 15/17 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Moderate correctness and test-isolation issues must be resolved before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (6)
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/requirements.txt:8
- These unbounded requirements can resolve incompatible future major releases. This package explicitly constrains sample dependencies (
CHANGELOG.md:40-43), and neighboring published-package samples use bounded ranges (for examplesamples/basic_voice_agent/requirements.txt:1); add compatible minimum and upper bounds here as well.
azure-ai-agentserver-core
azure-ai-agentserver-invocations
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/README.md:76
- Ctrl-C performs graceful host shutdown, not a hard crash. This task never checks
ctx.shutdownor callsexit_for_recovery(), so after the shutdown grace period TaskManager cancels it and deletes the ephemeral one-shot task record (core/tasks/_manager.py:2076-2101); a restart then has nothing to recover. Use an actual hard kill in this crash-recovery walkthrough, or add graceful-shutdown handling to the task.
2. Poll until `completed_steps` is a few in, then **kill the process** (Ctrl-C).
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/requirements.txt:8
- These unbounded requirements can resolve incompatible future major releases. This package explicitly constrains sample dependencies (
CHANGELOG.md:40-43), and neighboring published-package samples use bounded ranges (for examplesamples/basic_voice_agent/requirements.txt:1); add compatible minimum and upper bounds here as well.
azure-ai-agentserver-core
azure-ai-agentserver-invocations
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_langgraph/app.py:91
- This fallback description is incorrect for invocations:
get_task_manager()raises and this.start()call is not caught, so the request fails rather than running the agent non-durably (core/tasks/_manager.py:289-306). Reserve the non-durable fallback wording for protocols that explicitly swallow this exception.
# ``langgraph_session.start()`` raise ``TaskManagerNotInitialized`` and the agent
# runs non-durably with no crash recovery — defeating the purpose of a resilient
# long-running agent.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_multiturn/app.py:54
- This fallback description is incorrect for invocations:
get_task_manager()raises and this.start()call is not caught, so the request fails rather than running the agent non-durably (core/tasks/_manager.py:289-306). Reserve the non-durable fallback wording for protocols that explicitly swallow this exception.
# ``session_workflow.start()`` raise ``TaskManagerNotInitialized`` and the agent
# runs non-durably with no crash recovery — defeating the purpose of a resilient
# long-running agent.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_research/app.py:109
- This fallback description is incorrect for invocations:
get_task_manager()raises and this.start()call is not caught, so the request fails rather than running the agent non-durably (core/tasks/_manager.py:289-306). Reserve the non-durable fallback wording for protocols that explicitly swallow this exception.
# ``deep_research.start()`` raise ``TaskManagerNotInitialized`` and the agent
# runs non-durably with no crash recovery — defeating the purpose of a resilient
# long-running agent.
- Files reviewed: 15/17 changed files
- Comments generated: 17
- Review effort level: Balanced
Addresses the latest reviewer pass on the LRA samples: - Terminal failure state (both samples): a one-shot task record is deleted on terminal exit, so a task that raised left the durable checkpoint below its target and polling reported in_progress/running forever. The task now persists an explicit status (in_progress/completed/failed, plus stopped via marker for the forever worker) and the poll handler surfaces it. hello_world skips the terminal write when already finalized (idempotent, no ETag churn on recovery). - Reuse-after-terminal (both samples): create_item is now the authoritative existence gate. Because start() would NOT conflict after a completed/stopped one-shot record is deleted, a reused invocation id previously spun up a new task against a stale checkpoint/marker. On create_item conflict we now return 409 with the current status and never start a second task; start() is only reached when we actually created the record. - Invalid UTF-8 body -> 400 (both samples): json.loads on non-UTF-8 bytes raises UnicodeDecodeError; caught alongside JSONDecodeError so malformed bytes are 400, not 500. - Version fix: the opt-in breaking change landed in core 2.1.0b1, not 2.2.0b1. Corrected across hello_world, hello_forever, langgraph, multiturn, research (app.py comments + READMEs). - Structure test: the opt-in switch is read at host lifespan start, not at InvocationAgentServerHost() construction, so the enable-before-construction ordering assertion was not meaningful. Now only requires a real module-level set_resilient_tasks_enabled(True) that executes on import. - Tests: removed module-level os.environ.pop(FOUNDRY_HOSTING_ENVIRONMENT) from the three e2e files (collection-time global mutation outside pytest's restoration); the fixtures already delenv via monkeypatch. Added handler coverage for reuse 409, failed-status polling, and invalid-UTF-8 400. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8abde5e2-e955-42bc-957b-7bf6877cd753
There was a problem hiding this comment.
🟡 Changes recommended
One or more issues must be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (7)
Previously missed (2) — in code that hasn't changed since the last review.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/requirements.txt:8
- Bound both dependencies to compatible release lines. Unbounded requirements make this sample resolve future incompatible major versions, contrary to the package convention demonstrated by
samples/basic_voice_agent/requirements.txt:1and recorded inCHANGELOG.md:42-43.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/requirements.txt:8 - Bound both dependencies to compatible release lines. Unbounded requirements make this sample resolve future incompatible major versions, contrary to the package convention demonstrated by
samples/basic_voice_agent/requirements.txt:1and recorded inCHANGELOG.md:42-43.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py:234
- This treats every existing checkpoint as an active worker. If the checkpoint already has
status == "failed", cancel writes a stop marker; subsequent GETs prioritize that marker and incorrectly change the terminal result fromfailedtostopped. An already stopped worker is also reported ascancelling. Check the durable status and existing marker first, and return the existing terminal state without writing a new marker.
if await store.get_item(task_id) is None:
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/README.md:76
- Ctrl-C initiates the host's graceful lifespan shutdown, but this task never checks
ctx.shutdownor callsexit_for_recovery(). After the shutdown grace period,TaskManagercancels the execution and deletes the ephemeral task record, so the restart described here has nothing to recover. Either add graceful-shutdown handling to the task or instruct users to perform an actual hard kill.
2. Poll until `completed_steps` is a few in, then **kill the process** (Ctrl-C).
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_langgraph/app.py:91
- This says the agent runs non-durably, but
langgraph_session.start()raisesTaskManagerNotInitialized, so no task execution occurs. Reserve the non-durable fallback wording for protocol paths that explicitly catch this exception.
# startup (i.e. at module-import time). Without it, ``get_task_manager()`` /
# ``langgraph_session.start()`` raise ``TaskManagerNotInitialized`` and the agent
# runs non-durably with no crash recovery — defeating the purpose of a resilient
# long-running agent.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_multiturn/app.py:54
- This says the agent runs non-durably, but
session_workflow.start()raisesTaskManagerNotInitialized, so no task execution occurs. Reserve the non-durable fallback wording for protocol paths that explicitly catch this exception.
# startup (i.e. at module-import time). Without it, ``get_task_manager()`` /
# ``session_workflow.start()`` raise ``TaskManagerNotInitialized`` and the agent
# runs non-durably with no crash recovery — defeating the purpose of a resilient
# long-running agent.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_research/app.py:109
- This says the agent runs non-durably, but the preceding call raises
TaskManagerNotInitialized, so the durable task does not run at all. Reserve the non-durable fallback wording for protocol paths that explicitly swallow this exception.
# ``deep_research.start()`` raise ``TaskManagerNotInitialized`` and the agent
# runs non-durably with no crash recovery — defeating the purpose of a resilient
# long-running agent.
- Files reviewed: 15/17 changed files
- Comments generated: 3
- Review effort level: Balanced
A new minimal (core + invocations only, no LLM) long-running-agent sample that sits between hello_world (finite, runs to completion) and hello_forever (indefinite, must be stopped): a FINITE job that would finish on its own but can be cancelled mid-run. Cancel is cooperative and durable: the cancel endpoint writes a durable cancel marker to a separate state-store key; the task reads it before each step and, if present, records status="cancelled" and stops early. Using a durable marker (instead of the in-process ctx.cancel event) makes cancellation correct cross-replica and across crash/redeploy, and keeping it in its own key avoids racing the checkpoint's ETag. Built to the same robustness bar as the other minimal samples: user-isolated, session-scoped store; SHA-256 composite task id; atomic create_item existence gate returning 409 on reuse; explicit terminal status (completed/cancelled/ failed) so polling never sticks at in_progress; UTF-8 / body-shape / positive- integer-steps validation returning 400; call_id carried for recovery. Registered in the structure test's minimal-sample set. Adds task-level e2e tests (run to completion, cancel before start, cancel mid-run, recovery honours a pending cancel) and a handler-level lifecycle test (start -> cancel -> cancelling -> cancelled, reuse 409, 404, 400). Also hardened test_hello_world_handlers to drain the background job to a terminal state before the reuse-409 assertion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8abde5e2-e955-42bc-957b-7bf6877cd753
There was a problem hiding this comment.
🟡 Changes recommended
Moderate issues remain in start-failure reconciliation, checkpoint cursor handling, and dependency constraints.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (5)
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py:140
- The checkpoint is created before
start(), but onlyTaskConflictErroris handled. If provider access or scheduling raises any other exception, the request returns 500 while the durable record remainsrunning; retries are permanently rejected and polling reports a worker that does not exist. Reconcile the seed on every start failure, while handling the possibility that a remote start was accepted before a transport error.
try:
await hello_forever.start(
task_id=task_id,
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/README.md:76
- The documented
Ctrl-Cpath does not demonstrate crash recovery for this task.TaskManager.shutdown()signals graceful shutdown, waits up to its grace period, then cancels handlers that do not callexit_for_recovery(); cancellation deletes the ephemeral task record, leaving nothing for the startup recovery scan. Use an abrupt process/container kill in these instructions, or make the task shutdown-aware.
2. Poll until `completed_steps` is a few in, then **kill the process** (Ctrl-C).
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/agent.py:181
completedis captured only at task entry and is never advanced by the loop. If a later checkpoint or terminal write raises after earlier steps succeeded, this handler overwrites the durable cursor with that stale entry value, so polling regresses and the recorded resume point is lost. Use the just-fetched current item as the failure cursor.
"completed_steps": completed,
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/app.py:144
- The checkpoint is created before
start(), but onlyTaskConflictErroris handled. If provider access or scheduling raises any other exception, the request returns 500 while this durable item remainsin_progress; retries then hit the existence gate with 409 and polling reports a task that never started. Reconcile the seed on every start failure (for example, persist an explicit failed-to-start state or safely roll it back while accounting for an ambiguous remote accept).
try:
await hello_world.start(
task_id=task_id,
sdk/agentserver/azure-ai-agentserver-invocations/tests/test_resilient_samples_structure.py:56
- This makes
resilient_cancellablepart of the required shipping surface, but the PR description says that exactly two minimal samples are being added and does not mention or verify this third sample. Please either update the PR scope/verification to include the cancellable sample or remove it from this change.
"resilient_cancellable",
- Files reviewed: 20/23 changed files
- Comments generated: 6
- Review effort level: Balanced
[Pilot] PR Pipeline Failure AnalysisWhat failedAzure Pipeline build 6788933 ( Relevant pipeline outputRecommended next steps
Automated fix: Requested
|
CI (Analyze/cspell): renamed the test variable `fstore` -> `failed_store` (cspell flagged the coined word), fixing the failing Analyze stage. Substantive sample fixes from review: - Orphaned-seed recovery (all three minimal samples): the invoke handler seeds a durable record via create_item before start(). If start() crashed (or the process died) after the seed but before the TaskManager record was durable, the record stayed nonterminal forever, every retry got 409, and polling reported in_progress/running with nothing to recover. Now: create_item conflict with a TERMINAL status returns 409 (idempotent); a NONTERMINAL status falls through and (re-)schedules idempotently (start() recovers the orphan; TaskConflictError means a task is already running). On a non-conflict start() failure we delete the seed we created this request so a retry starts cleanly (safe under an ambiguous remote accept, since retry + TaskConflictError still converges). - Progress rollback on failure (hello_world, cancellable): the failure handler wrote the entry-time completed_steps (often 0), rolling durable progress backward. Now a local `done` cursor advances after each successful checkpoint and is used when recording failure. Nits: - Structure test AST guard now accepts set_resilient_tasks_enabled(enabled=True) (keyword form), not just positional True. - e2e tests use an autouse monkeypatch.syspath_prepend fixture instead of an unrestored sys.path.insert (no cross-test leakage). - requirements.txt for the three minimal samples now use compatible ranges (core>=2.1.0b1,<3.0.0; invocations>=1.2.0b1,<2.0.0) matching package convention. - hello_world README: clarify the TaskManager task id / checkpoint key is a hashed composite of user+session+invocation, not the raw invocation id. - cancellable README: the crash-durable-cancel demo requires SIGKILL, not Ctrl-C (graceful shutdown lets the task observe the marker and exit before restart). Added a handler test locking in orphan-seed recovery (202, runs to completion). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8abde5e2-e955-42bc-957b-7bf6877cd753
There was a problem hiding this comment.
🟡 Changes recommended
Six critical and four moderate findings remain in persistence concurrency, local storage, recovery, and cancellation behavior.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (3)
Previously missed (2) — in code that hasn't changed since the last review.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py:262
- This writes a stop marker even when the durable record is already
failedor already stopped. Becausehandle_getgives the marker precedence, cancelling a failed worker permanently changes its visible status fromfailedtostoppedand hides its error; repeated cancellation also reportscancellingfor a terminal worker. Check the persisted status and existing marker before writing, and return the existing terminal state instead.
sdk/agentserver/azure-ai-agentserver-invocations/tests/test_resilient_samples_structure.py:56 - This introduces
resilient_cancellableas a third complete sample, but the PR description and verification plan say that onlyresilient_hello_worldandresilient_hello_foreverare being added. Please either document and verify this additional scope or move it to a separate change.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/README.md:83
- Ctrl-C initiates graceful host shutdown rather than a crash. This task does not observe
ctx.shutdown; it either finishes during the grace period or is cancelled afterward, and one-shot cancellation deletes its TaskManager record, so the documented restart will have nothing to recover. Tell users to use a hard kill here, consistent with the cancellable sample's recovery instructions.
2. Poll until `completed_steps` is a few in, then **kill the process** (Ctrl-C).
- Files reviewed: 20/23 changed files
- Comments generated: 9
- Review effort level: Balanced
…otent Addresses this review pass across all three minimal samples: - Store-name length (agent.py): a protocol session id can be 256 chars, and the local FoundryStateStore backend base64-encodes the whole store name into one filename, overflowing the 255-byte NAME_MAX (ENAMETOOLONG) for this no-cloud sample. checkpoint_store_name now hashes the session component (SHA-256) so the name is fixed-width while remaining unique per session. - Failure write defeated checkpoint CAS (agent.py): the failure handler re-read the latest ETag before writing status=failed, so a stale/lost execution could clobber a newer owner's progress and mark the live run failed. It now writes with THIS execution's last-owned etag; if the etag moved, the best-effort write simply fails instead of overwriting newer state. - Non-idempotent orphan recovery (app.py): the nonterminal-conflict re-schedule path used the retry request's name/steps instead of the persisted durable input, so a retry reusing the invocation id with a different body could shorten, extend, or roll back an in-flight job (e.g. 5/10 retried with steps=3). It now reuses the persisted parameters (name/steps for hello_world & cancellable, name for hello_forever) before rescheduling. Strengthened the orphan-recovery test to POST a different body and assert the persisted params win (finishes at 5/5). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8abde5e2-e955-42bc-957b-7bf6877cd753
There was a problem hiding this comment.
🔵 Needs a closer look
The stop-state reporting and resilient-task opt-in regression guard have unresolved moderate-severity correctness issues.
Review details
Suppressed comments (8)
Previously missed (2) — in code that hasn't changed since the last review.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_forever/app.py:228
- The stop marker records a stop request, not that the worker has exited.
handle_cancelcan write it whilehello_foreveris still sleeping (and it may checkpoint another iteration), so this immediately reports terminalstopped; it also masks a persistedfailedstatus because the marker wins. Persiststatus: stoppedin the worker before returning, reportcancellingwhile only the marker exists, and preservefailedprecedence.
sdk/agentserver/azure-ai-agentserver-invocations/tests/test_resilient_samples_structure.py:290 - This accepts any literal
Trueanywhere in the call, so invalid shapes such asset_resilient_tasks_enabled(value=False, typo=True)satisfy the regression guard; the documentedenabled=Truekeyword would itself raise because the public parameter is namedvalue. Match exactly one positionalTrueorvalue=True.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_cancellable/README.md:1
- The PR description says this change adds two minimal samples (
resilient_hello_worldandresilient_hello_forever), but this introduces a third substantial cancellable sample and associated tests. Please update the title/description and change list to reflect this scope, or split this sample into a separate PR.
# Minimal resilient long-running agent — **cancellation**
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_hello_world/README.md:83
- Ctrl-C initiates graceful host shutdown rather than the hard crash this procedure intends to demonstrate. Because
hello_worlddoes not handlectx.shutdownwithexit_for_recovery, it can continue during the shutdown grace period, making this recovery walkthrough nondeterministic; use SIGKILL as in the verified scenario.
2. Poll until `completed_steps` is a few in, then **kill the process** (Ctrl-C).
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_langgraph/app.py:91
- These statements contradict each other: when
get_task_manager()/.start()raisesTaskManagerNotInitialized, this invocation agent does not run non-durably—it does not start. Describe the fail-fast behavior so users do not expect an in-process fallback that this API does not provide.
# ``TaskManager`` ONLY when this switch is on, and it must be set before host
# startup (i.e. at module-import time). Without it, ``get_task_manager()`` /
# ``langgraph_session.start()`` raise ``TaskManagerNotInitialized`` and the agent
# runs non-durably with no crash recovery — defeating the purpose of a resilient
# long-running agent.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_multiturn/app.py:54
- These statements contradict each other: when
get_task_manager()/.start()raisesTaskManagerNotInitialized, this invocation agent does not run non-durably—it does not start. Describe the fail-fast behavior so users do not expect an in-process fallback that this API does not provide.
# ``TaskManager`` ONLY when this switch is on, and it must be set before host
# startup (i.e. at module-import time). Without it, ``get_task_manager()`` /
# ``session_workflow.start()`` raise ``TaskManagerNotInitialized`` and the agent
# runs non-durably with no crash recovery — defeating the purpose of a resilient
# long-running agent.
sdk/agentserver/azure-ai-agentserver-invocations/samples/resilient_research/app.py:109
- These statements contradict each other: when
get_task_manager()/.start()raisesTaskManagerNotInitialized, this invocation agent does not run non-durably—it does not start. Describe the fail-fast behavior so users do not expect an in-process fallback that this API does not provide.
# startup (i.e. at module-import time). Without it, ``get_task_manager()`` /
# ``deep_research.start()`` raise ``TaskManagerNotInitialized`` and the agent
# runs non-durably with no crash recovery — defeating the purpose of a resilient
# long-running agent.
sdk/agentserver/azure-ai-agentserver-invocations/tests/test_resilient_samples_structure.py:56
- The PR description says this adds two minimal samples and verifies five resilient samples, but this third sample plus its implementation/tests makes six. Update the title, description, and verification scope to include
resilient_cancellable, or remove it from this PR, so the reviewed scope matches what ships.
"resilient_cancellable",
- Files reviewed: 20/23 changed files
- Comments generated: 0 new
- Review effort level: Balanced
What & why
The durable-task subsystem is strictly opt-in as of
azure-ai-agentserver-core2.2.0b1:AgentServerHostonly constructs theTaskManager(and runs the crash-recovery scan) whenset_resilient_tasks_enabled(True)is called before host startup. With the switch off,get_task_manager()/<task>.start()raiseTaskManagerNotInitializedand durable execution + crash recovery are inactive.The resilient invocations samples declare durable tasks (
@task/@multi_turn_task) but never enable the subsystem, and there is no host auto-enable for invocations. As shipped, they would raiseTaskManagerNotInitializedand would not actually run as long-running/crash-recoverable agents.Changes
Enable the subsystem in the existing resilient samples (one line, at import time, before
app = InvocationAgentServerHost()):resilient_research/app.pyresilient_multiturn/app.pyresilient_langgraph/app.pyAdd two minimal LRA samples that depend on only
azure-ai-agentserver-core+azure-ai-agentserver-invocations(no LLM, noazure-ai-projects, nolanggraph) — a low-footprint on-ramp:resilient_hello_world— smallest start-and-poll LRA: a durable@taskthat counts N steps, checkpointing to a state store after each, and resumes onentry_mode == "recovered".resilient_hello_forever— minimal indefinite durable worker: awhile Trueloop with per-iteration checkpointing, graceful-shutdown handling (ctx.shutdown→exit_for_recovery()), and a cancel path.Verification (local, against core 2.2.0b1)
resilient_hello_worldandresilient_hello_forever: resume from their checkpoint after a hardkill(recovery re-enters withentry_mode == "recovered"and continues past the crash point rather than restarting).resilient_hello_forever: stops on cancel.resilient_research: streams end-to-end (token/stage events) once enabled.Notes
set_resilient_tasks_enabled(True); this brings the invocations samples in line.