Skip to content

Python: include checkpoint_id on AG-UI interrupt metadata (#8150) - #8163

Open
lsmlhi_25 (FOWEPJF255) wants to merge 5 commits into
microsoft:mainfrom
FOWEPJF255:fix/ag-ui-interrupt-checkpoint-8150
Open

Python: include checkpoint_id on AG-UI interrupt metadata (#8150)#8163
lsmlhi_25 (FOWEPJF255) wants to merge 5 commits into
microsoft:mainfrom
FOWEPJF255:fix/ag-ui-interrupt-checkpoint-8150

Conversation

@FOWEPJF255

@FOWEPJF255 lsmlhi_25 (FOWEPJF255) commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Motivation & Context

AG-UI workflow interrupts need a durable pause checkpoint_id so multi-worker clients can resume via forwardedProps.checkpoint_id without a side-channel lookup (Option A from #8150).

Description & Review Guide

  • What are the major changes?
    • Attach the pause checkpoint_id to each interrupt's metadata.agent_framework when AG-UI workflow runs finish with interrupts and checkpoint storage is active.
    • Prefer this runner's last-saved pause checkpoint over shared get_latest(workflow_name=...) (avoids cross-owner races / stale ids).
    • Resolve builder-configured checkpoint storage via the workflow runner when run_workflow_stream does not pass checkpoint_storage.
    • Skip storage work when interrupts is empty; remove the unused checkpoint_id kwarg on _workflow_interrupt_metadata.
  • What is the impact of these changes?
    • Clients can round-trip the pause checkpoint id from interrupt metadata for durable resume across workers.
  • What do you want reviewers to focus on?
    • Correctness of pause-id selection vs shared get_latest, builder-only storage path, and empty-interrupt short-circuit.

Related Issue

Fixes #8150

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) 鈥?a workflow keeps the label and title prefix in sync automatically.

…8150)

Attach the pause workflow checkpoint id to RUN_FINISHED interrupt
metadata.agent_framework so multi-worker clients can resume via
forwardedProps.checkpoint_id without a side channel.

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.

🟡 Changes recommended

Checkpoint selection can advertise a stale or different request owner’s checkpoint ID.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds checkpoint IDs to AG-UI workflow interrupt metadata for durable multi-worker resume.

Changes:

  • Resolves and attaches pause checkpoint IDs to interrupts.
  • Adds helper and checkpoint-resume coverage.
File summaries
File Description
_workflow_run.py Adds checkpoint lookup and interrupt metadata enrichment.
test_workflow_run.py Tests attachment and checkpoint ID propagation.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread python/packages/ag-ui/agent_framework_ag_ui/_workflow_run.py Outdated
Comment thread python/packages/ag-ui/agent_framework_ag_ui/_workflow_run.py Outdated
Comment thread python/packages/ag-ui/agent_framework_ag_ui/_workflow_run.py Outdated
Comment thread python/packages/ag-ui/agent_framework_ag_ui/_workflow_run.py Outdated
@FOWEPJF255

Copy link
Copy Markdown
Contributor Author

Addressed review feedback:

  1. Stop using shared get_latest(workflow_name=...) (races across owners / stale ids). Prefer this runner's _previous_checkpoint_id and only advertise it when the checkpoint's pending request set covers the interrupt ids.
  2. Resolve builder-configured storage via the workflow runner when run_workflow_stream does not pass checkpoint_storage.
  3. Skip storage work when interrupts is empty; remove the unused checkpoint_id kwarg on _workflow_interrupt_metadata.
  4. Added regression tests for competing shared latest + builder-only storage.

@moonbox3

Evan Mattson (moonbox3) commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

lsmlhi_25 (@FOWEPJF255):

  1. Please update the PR body to use our approved template: https://github.com/microsoft/agent-framework/blob/main/.github/pull_request_template.md
  2. Per our contributing guidelines, please respond to all open PR comments as to whether they're addressed or not. Then resolve the threads once done.
  3. Please also fix the failing code quality checks.

@FOWEPJF255

Copy link
Copy Markdown
Contributor Author

Evan Mattson (@moonbox3) Thanks 鈥?addressed your checklist:

  1. PR body updated to the approved template.
  2. Replied on each review thread and resolved them.
  3. Pushed a follow-up for the failing code quality checks: ruff I001 (import blank line) and ty ignores on the new yield_output("done") test helpers.

Comment thread python/packages/ag-ui/agent_framework_ag_ui/_workflow_run.py Outdated
Comment thread python/packages/ag-ui/agent_framework_ag_ui/_workflow_run.py Outdated
Move pause-checkpoint selection into Workflow and require a run-scoped
baseline so leftover runner ids are not advertised across runs.
@FOWEPJF255

Copy link
Copy Markdown
Contributor Author

Evan Mattson (@moonbox3) Follow-up for the run-scoped / core-helper review:

  • Workflow.get_last_checkpoint_id() + Workflow.resolve_pause_checkpoint_id(...) in core (storage precedence, pending-request coverage, run-scoped baseline filter).
  • AG-UI captures the baseline before workflow.run() and no longer digs into Runner private fields for pause-id selection.
  • Regression tests in core test_pause_checkpoint_resolve.py and AG-UI run-scoped coverage.

Commit: 68f5abe.

Comment on lines +1323 to +1346
candidates: list[str] = []
if runner_candidate is not None:
candidates.append(runner_candidate)
if known_checkpoint_id is not None and known_checkpoint_id not in candidates:
candidates.append(str(known_checkpoint_id))

if storage is None and not use_context_storage:
# Without storage we cannot prove coverage; only advertise a run-scoped runner id.
return runner_candidate

for candidate in candidates:
try:
if storage is not None:
checkpoint = await storage.load(candidate)
else:
checkpoint = await self._runner.context.load_checkpoint(candidate)
except Exception: # pragma: no cover - storage/type drift
logger.debug("Could not load pause checkpoint candidate %s", candidate, exc_info=True)
continue
if checkpoint is None:
continue
pending = checkpoint.pending_request_info_events or {}
if ids.issubset({str(key) for key in dict(pending)}):
return candidate

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.

Following up on the run-scoping feedback: could we exclude the incoming restored checkpoint from these candidates after execution resumes? A supported response handler can reissue the same request ID, and if the new pause save fails, known_checkpoint_id still loads the old checkpoint and the key-only coverage check at _workflow.py:1344-1346 accepts it. The emitted ID then restores pre-response state, so tracking the checkpoint successfully created by this run rather than the restored ID would avoid replaying stale work.

Comment on lines +1272 to +1288
def get_last_checkpoint_id(self) -> str | None:
"""Return the checkpoint id last persisted or restored by this workflow runner.

Capture this value before ``run()`` when a host needs a run-scoped pause id:
after the run, only a different id indicates *this* run advanced the chain.
"""
checkpoint_id = self._runner._previous_checkpoint_id # pyright: ignore[reportPrivateUsage]
return str(checkpoint_id) if checkpoint_id is not None else None

async def resolve_pause_checkpoint_id(
self,
request_ids: Collection[str],
*,
checkpoint_storage: CheckpointStorage | None = None,
known_checkpoint_id: str | None = None,
baseline_checkpoint_id: str | None = None,
) -> str | None:

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.

Would it make sense for Workflow to own the run boundary used by resolve_pause_checkpoint_id()? Every caller now has to call get_last_checkpoint_id() before run(), retain it across every exit path, and pass it back as baseline_checkpoint_id; mistiming that sequence lets a restored checkpoint look newly persisted. Tracking the checkpoint created by the most recent run inside Workflow would remove that ordering protocol while preserving the AG-UI behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: AG-UI: include checkpoint_id on RUN_FINISHED interrupts for multi-worker resume

4 participants