Skip to content

[Feature]: Support caller-provided stable identity for durable operations #573

Description

@zhongkechen

What would you like?

Add an optional caller-provided stable identity for every durable operation
created through DurableContext, so logical work can map to the same checkpoint
independently of the order in which concurrent calls reach the SDK.

For example, operation APIs could consistently accept a logical key:

tool_result = context.step(
    run_tool,
    name="tool.fetch_weather",
    key=tool_call_id,
)

context.wait(duration, name="rate-limit", key="provider-rate-limit")
context.invoke(function_name, payload, name="worker", key=request_id)

The exact parameter name and shape are open for design. A logical key that the
SDK scopes and hashes is preferable to accepting an arbitrary raw operation ID,
because the SDK can preserve service constraints, parent scoping, and collision
protection.

This should cover all public context APIs that create durable operations,
including step, wait, invoke, create_callback, run_in_child_context,
wait_for_callback, wait_for_condition, map, and parallel.

Today operation IDs are generally derived from the context's step-ID prefix and
a monotonically increasing invocation counter. The name is observability
metadata and does not provide identity. This makes concurrently scheduled
logical operations sensitive to scheduling order: if calls reach the SDK in a
different order during replay, they can be assigned one another's checkpoints.

The immediate use case is Pydantic AI durability. Tool calls have a stable
tool_call_id, but the AWS Lambda integration must currently force sequential
tool execution so operation ordering remains replay-compatible. Stable
caller-provided identity would allow those independent tool calls to remain
parallel. The same capability would help other async frameworks and workflow
adapters whose logical work already has deterministic IDs.

Desired properties:

  • The same key in the same parent context deterministically produces the same
    operation ID across replay.
  • Keys are scoped by parent context and operation subtype.
  • Concurrent keyed operations may be reached and complete in different orders.
  • Duplicate use of a key within one context/invocation fails clearly.
  • Keyed and existing sequence-identified operations can coexist.
  • Omitting the key preserves current behavior and replay compatibility.
  • Replay still validates operation subtype and relevant metadata consistently.
  • Composite APIs propagate identity deterministically to the operation tree they
    create.
  • map iterations and parallel branches have a way to use stable item/branch
    identity when index-based identity is insufficient.

Possible Implementation

Extend the DurableContext protocol and implementation with a consistent
optional keyword such as key on operation-creating methods. A shared internal
operation-identity abstraction should avoid implementing subtly different
identity rules for each API.

For keyed operations, derive the operation ID from a versioned domain separator,
context step-ID prefix/parent, operation subtype, and caller key, using the SDK's
existing bounded hash format. Do not expose the raw service operation ID unless
there is a compelling need.

The replay-aware lookahead currently assumes that the next operation ID comes
from the sequence counter. It would need to accept the known keyed operation ID
when entering a keyed operation. The design should also decide whether keyed
operations consume the positional counter; not consuming it would keep unrelated
positional identities stable when keyed work is added or removed.

Operation trees need explicit identity semantics:

  • For run_in_child_context, the caller key can identify the outer operation
    and child context, with nested operation IDs continuing to derive from that
    stable parent.
  • For wait_for_callback, the caller key can identify its outer
    RUN_IN_CHILD_CONTEXT operation, with callback internals deriving beneath
    that stable parent.
  • For wait_for_condition, the caller key directly identifies the polling
    operation.
  • For map, consider an item_key callback analogous to item_namer, but used
    for identity rather than display.
  • For parallel, consider a key on ParallelBranch.
  • Alternatively, define a smaller first phase that keys only the parent
    map/parallel operation and clearly retains index-based child identity.

Suggested tests:

  • Reaching the same keyed operations in opposite scheduling orders reuses the
    correct checkpoints for every operation subtype.
  • Keyed operations are distinct across parent contexts and operation subtypes.
  • Duplicate keys are rejected.
  • Keyed and positional operations can be mixed deterministically.
  • Composite operations produce stable nested IDs.
  • Reordered keyed map items and parallel branches reuse the intended
    checkpoints when item/branch identity is enabled.
  • Existing unkeyed executions retain their current operation IDs.
  • Replay divergence still produces a clear error for incompatible operation
    metadata.

Is this a breaking change?

No. The new identity input would be optional and existing operation calls would
retain their sequence-derived IDs.

Does this require an RFC?

Yes. It changes the operation identity model and interacts with replay-boundary
detection, concurrency, collision handling, and compatibility.

Additional Context

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions