Skip to content

feat(durable): wait for callbacks with ctx.wait_for_callback - #400

Merged
subnetmarco merged 6 commits into
mainfrom
feat/durable-callbacks
Oct 26, 2026
Merged

subnetmarco merged 6 commits into
mainfrom
feat/durable-callbacks

Conversation

@subnetmarco

@subnetmarco subnetmarco commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Why

Customers migrating workflows asked for task-token style callbacks: a durable step hands a token to another system, which later succeeds it, fails it, or heartbeats it. Hosting adds the token routes in Kong/volcano-hosting#1966. This PR is the Python authoring API for them.

Summary

  • ctx.wait_for_callback(name, submitter, *, timeout=None, heartbeat_timeout=None) registers the callback and hands its vcb_ token to submitter. It then suspends until the token is answered.
    • On success, it returns the result JSON.
    • On timeout or heartbeat timeout, it returns None, the same as wait_for_message.
    • On failure, it raises CallbackFailedError from volcano_sdk.durable_authoring, with .type and .message.
  • The submitter runs inside a checkpointed step. It may run again on a retry, and a retried registration returns the same token.
  • Two chore(openapi) commits sync the spec and generated client from hosting.
  • README and docs/functions.md get a callbacks section.

Rollout: merge this PR and JS first, then release (0.16.0 under release-please), then merge the hosting PR, whose cloud E2E pins ~=0.16.0.

Verification

Tested commits:

  • SDK f18a34b, on main 11655d3. The feature commit is 2416ace; f18a34b only syncs an error description in the spec.
  • hosting 2bfbbffb3

Results:

  • uv run --locked poe checks passes: policy, audit, generated client, ruff, mypy, basedpyright, and the package checks.

  • pytest passes 2,995 tests at 100% coverage.

  • poe mutation passes on each changed module, with no survivors:

    Module Killed Rejected by type checks
    _durable_callback_token 33 39
    durable_authoring 149 210
    _durable_modules 42 11
    _durable_engine 78 134

    _durable_protocols holds only protocol declarations and has nothing to mutate.

  • Hosting runs the four shared scenarios, SDK-DURABLE-022 to 025, against this binding.

Not run: cloud E2E and the live staging sdk-contract runner.

Related: Kong/volcano-hosting#1966, Kong/volcano-sdk-js#332.

@github-actions
github-actions Bot requested a review from swkeever October 19, 2026 22:31
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

AI review: no P0-P2 findings

Reviewed 0f5fd6e. Run log

This PR adds ctx.wait_for_callback() and CallbackFailedError. A durable step registers with Volcano for a vcb_ callback token, passes it to a synchronous submitter, then suspends until another system succeeds the callback, fails it, or lets it time out. On a replay, the registration also sends the callback's operation id. The rest is an OpenAPI and generated-client sync from hosting, plus docs. The hand-written code reuses the existing approval and message wait patterns, its tests are thorough, and I found no defects. The main risk is its reliance on the runtime's private internals and on hosting's contract, which this review cannot see.

Not checked: The AI review read the diff, the code, and the PR's text; it ran no tests or builds and read nothing outside this repository.

  • Whether every runtime version >=2.0.0 exports CallbackExternalError; if one lacks it, load_exceptions raises TypeError for every durable function.
  • Whether the runtime keeps the private _peek_next_operation_id and op-id hashing that replayed_operation copies; if not, replays silently stop sending operation_id.
  • Whether hosting#1966 sends results as JSON documents and failure data as {type, message}, and honours operation_id on replay.
  • Whether releasing the SDK and docs before hosting deploys /durable-callbacks leaves early callers getting 404s, then CallbackSubmitterError.
  • Compatibility with Kong/volcano-hosting#1966 remains unverified, including interrupted-replay registration and callback outcome formats.

Human review: needed

  • Should possession of a callback token alone authorize completing, failing, or heartbeating a durable wait (openapi/openapi.yaml)?
  • Should scoped service keys authorize client-IP forwarding on the synced authentication routes (openapi/openapi.yaml)?

The AI review found no P0-P2 problems at 0f5fd6e, so the code needs no second review for bugs. Decide the points above: approve to accept them, or request changes.

Suggested reviewer: swkeever (wrote most of durable_authoring.py and the durable callback adapters). Any collaborator with write access other than the author can approve.

@subnetmarco subnetmarco left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This matches sdk-js#332 and the hosting contract where it matters:

  • the same error codes and 30-second registration deadline;
  • registration inside the checkpointed submitter step, so a replay doesn't register twice;
  • a timeout returns None, and callback_closed skips the submitter;
  • failures surface as CallbackFailedError from errorData;
  • the same name rules.

One real issue: async submitters (inline).

The Aikido finding on test_durable_callback_token.py:25 is a made-up fixture token against api.volcano.test, so it's safe to dismiss.

Release order matters here. Hosting#1966's cloud E2E pins volcano-sdk-python~=0.16.0, and sdk-python#398 (fix(sandboxes)!) would take 0.16.0 if it releases first. Release this one first, or re-pin hosting. Details on hosting#1966.

@subnetmarco subnetmarco left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Re-reviewed at 7279cfd. Refusing an async def submitter is the right call. Python durable authoring is synchronous throughout, and the runtime calls the submitter on a worker thread with no event loop, so running the coroutine there would break clients bound to another loop. The refusal is loud and comes before the callback opens, and it can be relaxed later without breaking anyone. Closing a returned coroutine in handed_off keeps the "never awaited" warning away, and the tests cover a coroutine function, a returned coroutine and a non-coroutine awaitable. CI is green apart from ai-review/human.

Release this before sdk-python#398. With bump-minor-pre-major, whichever releases first takes 0.16.0, so if #398 goes alone first, callbacks land in 0.17 and hosting#1966's docs are wrong. Note that 0.15.1 (Oct 21) satisfies hosting#1966's current >0.15.0 pin without callbacks; I've asked for that pin to name the actual release.

The description still says hosting pins ~=0.16.0. Please update it.

subnetmarco and others added 6 commits October 22, 2026 12:06
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The runtime calls the submitter on a thread without an event loop, and its result was discarded, so an async def submitter returned a coroutine that never ran: the wait opened and nothing received the token. Refuse an async def submitter with a TypeError before the callback opens, and fail the hand-off when a submitter returns any other awaitable, closing a coroutine so it does not warn later.

The submitter stays typed as returning object, as in the JavaScript SDK, so one written as an expression that returns what it started still type-checks. The token fixture now reads as the fake it is.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

@swkeever swkeever left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Guided review of 0f5fd6e. The AI review cleared correctness with no P0-P2 findings; these are the decisions it left to a person.

  1. Should possession of a callback token alone authorize completing, failing, or heartbeating a durable wait? Accept.
    Token-only is what lets a third party call back without a Volcano credential. Each token covers one step, takes one answer, and expires. Hardening, such as confirming a replayed registration, belongs on Kong/volcano-hosting#1966.
  2. Should scoped service keys authorize client-IP forwarding on the synced authentication routes? Accept.
    The contract already shipped in Kong/volcano-hosting#1747. This PR only regenerates the private _generated client, and no public SDK method sends the headers.

Generated by the guided-review skill. Install it with one command:

sh -c 'd=$(mktemp -d) && gh repo clone Kong/volcano-conventions "$d" -- --depth 1 -q && make -C "$d" install-skills guided-review pr-review explain-pr; s=$?; rm -rf "$d"; exit $s'

Then ask your coding agent for a guided review of a PR or issue URL. Run the command again to update.

@subnetmarco
subnetmarco added this pull request to the merge queue Oct 26, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 26, 2026
@subnetmarco
subnetmarco added this pull request to the merge queue Oct 26, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 26, 2026
@subnetmarco
subnetmarco added this pull request to the merge queue Oct 26, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 26, 2026
@subnetmarco
subnetmarco added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit b8f3430 Oct 26, 2026
100 checks passed
@subnetmarco
subnetmarco deleted the feat/durable-callbacks branch October 26, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants