Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/human-in-the-loop/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ async def cancel_order(ctx: Context, order_id: str, note: str) -> str:

async def fulfill_order(ctx: Context, order_id: str, amount: int) -> str:
# Open the human-decision promise first so its id is deterministic
# (``{workflow_id}.1``). ctx.promise returns a future whose ``id()`` is
# (``{workflow_id}:1``). ctx.promise returns a future whose ``id()`` is
# awaitable; we publish that id through a leaf so a real reviewer would
# know where to resolve.
approval = ctx.promise() # inherit workflow timeout
Expand Down
8 changes: 4 additions & 4 deletions examples/structured-concurrency/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
``foo`` awaited them.

We prove it durably. ``ctx.run`` children get deterministic ids
``{foo_id}.1`` and ``{foo_id}.2``. After ``foo`` returns ``5`` we attach to
``{foo_id}:1`` and ``{foo_id}:2``. After ``foo`` returns ``5`` we attach to
those two promises by id and assert each one resolved -- evidence the
never-awaited work was awaited *by the runtime* on our behalf.

Expand Down Expand Up @@ -74,11 +74,11 @@ async def main() -> None:

# Structured concurrency: the runtime awaited the two never-awaited
# ctx.run children before resolving foo. ctx.run assigns child ids in
# call order as ``{parent_id}.{seq}`` (seq starts at 1), so foo's two
# children are ``{foo_id}.1`` and ``{foo_id}.2``. Attach to each
# call order as ``{parent_id}:{seq}`` (seq starts at 1), so foo's two
# children are ``{foo_id}:1`` and ``{foo_id}:2``. Attach to each
# durable promise and confirm it resolved with bar's result.
for seq, n in ((1, 1), (2, 2)):
child_id = f"{foo_id}.{seq}"
child_id = f"{foo_id}:{seq}"
child_handle = await r.get(child_id)
child_out = await child_handle.result()
assert child_out == n * 10, (
Expand Down
27 changes: 17 additions & 10 deletions src/resonate/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def __init__(
# (which ``detached`` resets to the child's own id, starting a new
# lineage), while ``prefix_id`` propagates *unchanged* across
# ``detached`` re-roots. That keeps recursive ``detached`` ids bounded:
# every level mints ``{prefix}.{16hex}`` off the same fixed prefix
# every level mints ``{prefix}:{16hex}`` off the same fixed prefix
# rather than off its own grown id (see :meth:`Context.detached`). For
# any non-detached context the two are equal.
self.prefix_id = prefix_id
Expand Down Expand Up @@ -213,7 +213,7 @@ def root(
# ``prefix_id`` is the id-generation prefix, carried through the
# ``resonate:prefix`` tag. Unlike the lineage origin it is propagated
# *unchanged* across ``detached`` re-roots -- which is what keeps
# recursive ``detached`` ids bounded (``{prefix}.{16hex}``, one segment
# recursive ``detached`` ids bounded (``{prefix}:{16hex}``, one segment
# past the fixed prefix) rather than growing a segment per level. For a
# genuine top-level root it equals ``id`` (and ``origin_id``).
#
Expand Down Expand Up @@ -359,7 +359,13 @@ def get_dependency[T](self, type: type[T]) -> T:

def _next_id(self) -> str:
self._state.seq += 1
return f"{self._state.id}.{self._state.seq}"
# The id is ``<promiseId>:<lineage>``: a single ``:`` separates the
# promiseId (the lineage origin) from the lineage, and ``.`` separates
# lineage segments. So the first segment minted off a bare promiseId
# (``id == origin_id``) uses ``:`` (``root`` -> ``root:1``) and every
# deeper segment uses ``.`` (``root:1`` -> ``root:1.1``).
sep = ":" if self._state.id == self._state.origin_id else "."
return f"{self._state.id}{sep}{self._state.seq}"

async def flush_local_work(self) -> None:
"""Wait for every eagerly spawned task on this context to finish.
Expand Down Expand Up @@ -775,13 +781,14 @@ def detached(self, fn: str, *args: Any, **kwargs: Any) -> ResonateFuture[str]:
link = self._state.chain.link()

# Mint the id off ``prefix_id`` -- set at the top and propagated unchanged
# forever -- as ``{prefix}.d{16hex}``, so recursion stays bounded at one
# segment past the prefix instead of growing a segment per level. The
# ``d`` marks the segment as a detached child (vs an rpc child's numeric
# ``.{seq}``). ``resonate:origin`` is the child's own id (a fresh lineage
# root: ``origin == id == branch``); ``resonate:prefix`` (set in
# ``_global_req``) carries the same prefix forward to the next level.
child_id = f"{self._state.prefix_id}.d{_hash_id(self._next_id())}"
# forever -- as ``{prefix}:d{16hex}``, so recursion stays bounded at one
# segment past the prefix instead of growing a segment per level. The ``:``
# is the promiseId->lineage boundary and the ``d`` marks the segment as a
# detached child (vs an rpc child's numeric ``:{seq}``). ``resonate:origin``
# is the child's own id (a fresh lineage root: ``origin == id == branch``);
# ``resonate:prefix`` (set in ``_global_req``) carries the same prefix
# forward to the next level.
child_id = f"{self._state.prefix_id}:d{_hash_id(self._next_id())}"
req = self._global_req(
child_id,
self.opts.timeout,
Expand Down
2 changes: 1 addition & 1 deletion src/resonate/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ async def execute_until_blocked_inner(
origin_id=promise.tags.get("resonate:origin", promise.id),
# The id-generation prefix from ``resonate:prefix``. Unlike origin it
# is propagated *unchanged* across ``detached`` re-roots, so every
# recursion level mints ``{prefix}.{16hex}`` off the same fixed prefix
# recursion level mints ``{prefix}:{16hex}`` off the same fixed prefix
# instead of off its own grown id -- this is what bounds recursive
# detached ids. Falls back to ``promise.id`` when absent (genuine
# top-level root / tag-less promise), matching the origin fallback.
Expand Down
6 changes: 3 additions & 3 deletions src/resonate/network/nats.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@


def _id_to_origin(id: str) -> str:
"""Return the lineage origin: the substring before the first ``.``."""
dot = id.find(".")
return id if dot == -1 else id[:dot]
"""Return the lineage origin (promiseId): the substring before the first ``:``."""
sep = id.find(":")
return id if sep == -1 else id[:sep]


def _routing_origin(req: dict[str, Any]) -> str:
Expand Down
10 changes: 5 additions & 5 deletions tests/ext/test_pydantic_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ def explode(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse:
@pytest.mark.asyncio
async def test_model_request_replay_is_served_from_journal() -> None:
stored = ModelResponseEnvelope(response=_text_response("from the journal"))
ctx = _root([_resolved("root.1", stored)])
ctx = _root([_resolved("root:1", stored)])

model = ResonateModel(
_forbidden_model(), retry_policy=None, get_event_stream_handler=lambda: None
Expand All @@ -585,7 +585,7 @@ async def test_model_request_replay_is_served_from_journal() -> None:
@pytest.mark.asyncio
async def test_model_request_stream_replay_is_served_from_journal() -> None:
stored = ModelResponseEnvelope(response=_text_response("streamed from the journal"))
ctx = _root([_resolved("root.1", stored)])
ctx = _root([_resolved("root:1", stored)])

handler_calls = 0

Expand Down Expand Up @@ -653,9 +653,9 @@ async def test_replay_continues_from_last_completed_step() -> None:
parts=[ToolCallPart(tool_name="get_weather", args={"city": "SF"})]
)
)
# The first model request is `root.1`; leaving `root.2` unsettled forces the
# The first model request is `root:1`; leaving `root:2` unsettled forces the
# second request to run live.
ctx = _root([_resolved("root.1", step_one)])
ctx = _root([_resolved("root:1", step_one)])

model_calls = 0
tool_calls: list[str] = []
Expand Down Expand Up @@ -774,7 +774,7 @@ def model_logic(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse:
@pytest.mark.asyncio
async def test_mcp_call_tool_replay_is_served_from_journal() -> None:
stored = ToolResultEnvelope(result="42")
ctx = _root([_resolved("root.1", stored)])
ctx = _root([_resolved("root:1", stored)])

toolset = StubMCPToolset(id="calculator")
durable_toolset = ResonateMCPToolset(toolset)
Expand Down
Loading
Loading