agentsclimarketplace

Resonate basic debugging python

Skill resonatehq/resonate-skills/resonate-basic-debugging-python

Agent skills for building with Resonate — durable execution for long-running, crash-safe workflows.

Install
npx -y skills add resonatehq/resonate-skills --skill resonate-basic-debugging-python

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Debug and troubleshoot Resonate applications using the Python SDK v0.7.0 and Resonate Server v0.9.x. Use when investigating stuck workflows, non-deterministic replays, registration errors, async/await mistakes, unexpected retries, or server connectivity issues.

The file declares its own license as Apache-2.0. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

9.4 KB, as published. Nobody here has run it

Resonate Debug and Troubleshoot — Python

Overview

Use this skill to diagnose Resonate runtime issues in Python. The Python SDK v0.7.0 uses async def + await (not generators); failure modes differ from TypeScript's generator-based v1 API and from the legacy v0.6.x Python SDK. Focus on reproducible symptoms, promise state, registration, and determinism.

For the language-agnostic replay + recovery mental model, read durable-execution first.

Triage flow

  1. Classify the failure: import/syntax error, runtime SDK error, server error code, workflow stuck/hanging, or unexpected replay behavior.
  2. Confirm Python is ≥ 3.12 and resonate-sdk>=0.7.0 is installed.
  3. Confirm the Resonate server is reachable: resonate dev starts it on localhost:8001 (Rust v0.9.x, single port — no separate store port).
  4. Confirm the worker process is running, connected to the right URL and group, and registered for the right functions.
  5. Inspect the promise state and call graph for the invocation ID.
  6. Validate determinism — no time.time(), no random.random(), no un-checkpointed side effects in the orchestrator body.

Version + compat pins

  • Python ≥ 3.12 — the SDK uses recent typing syntax. 3.11 and below won't import.
  • resonate-sdk>=0.7.0 — async/await rewrite; incompatible with v0.6.x API.
  • Resonate server v0.9.x (Rust, single port 8001) — run with resonate dev for local development.
  • resonate-sdk on PyPI (not resonate — the bare name is a different package).

If from resonate.resonate import Resonate fails, you either have the wrong package installed or you're on Python < 3.12.

Async/await pitfalls (Python-specific)

These are the most common Python-only footguns with the v0.7.0 SDK.

Forgot await before a Context call

async def foo(ctx: Context, arg: str) -> str:
    ctx.run(bar, arg)   # missing await; returns a future the SDK sees but you lose the result
    return "done"

The future is tracked by structured concurrency (the runtime will still join it before settling foo), but the result is lost and your orchestrator continues without it. Fix:

result = await ctx.run(bar, arg)

Used yield instead of await

async def foo(ctx: Context, arg: str) -> str:
    result = yield ctx.run(bar, arg)    # wrong — this is generator syntax

The v0.7.0 SDK uses async def + await, not def + yield. Mixing them produces a TypeError or unexpected behavior. Use await.

Made the function a plain def instead of async def

def foo(ctx: Context, arg: str) -> str:   # missing async
    result = await ctx.run(bar, arg)       # SyntaxError

All durable functions must be async def. The SDK registers and dispatches them as coroutines.

Not awaiting r.promises.*

r.promises.resolve(approval_id, Value(data={"ok": True}))   # missing await

All r.promises.* methods are async. Missing await means the resolve is never sent. Fix:

await r.promises.resolve(approval_id, Value(data={"ok": True}))

Not calling await r.stop() on shutdown

The SDK needs to drain in-flight work cleanly. Always:

try:
    result = await r.run(id, fn, arg).result()
finally:
    await r.stop()

Registration errors

SymptomLikely causeFix
Function 'X' is not registeredrpc target name doesn't match a registered name on any reachable workerCheck r.register(fn) is called; ensure the calling name and registered name match (if you used r.register(fn, name="custom") the caller must use "custom")
Function version must be greater than zeroPassing version=0 explicitlyDefault is 1; omit version or pass a positive integer
Args unencodeableArgument to run/rpc not JSON-serializableUse dicts, lists, strings, numbers, bools, None, or types with custom encoders registered

Promise-state errors

CodeMeaningTypical cause
40900Promise already existsTwo calls used the same invocation ID; treat as idempotency — the second call returns the first's result. If you wanted a fresh run, change the ID
40400Promise not foundWrong ID, wrong server URL, or promise was purged
40300Already resolvedNormal if you're checking idempotency; the value is the first resolution
40301Already rejectedNormal; the reason is the first rejection
40303Already timed outPromise's deadline passed; re-create with a new ID if you want to retry

Inspect from the CLI:

resonate promises get <promise-id>
resonate tree <invocation-id>
resonate promises search <id-prefix>

"My workflow hangs"

Three common causes, in order of likelihood:

  1. No worker for the target group. If your rpc targets "backend" but no process has registered with group="backend", the invocation is queued forever. Check: resonate promises get <id> → state stays pending; confirm your worker process is running and connected.
  2. Forgot await (see pitfalls above). The orchestrator advanced past a step without waiting for it.
  3. Server not reachable. Confirm resonate dev is running on port 8001; check RESONATE_URL env var.

"My workflow replays things it shouldn't"

Replays are normal — Resonate re-executes the durable function from the top on every resumption, using stored promise values for the awaited checkpoints. Side effects outside the ctx.run envelope re-execute on every replay.

async def foo(ctx: Context, arg: str) -> str:
    print(f"starting {arg}")              # prints on every replay
    log.info(f"starting {arg}")           # logs on every replay
    result = await ctx.run(work, arg)
    return result

Fix: move any side effect into a helper called via ctx.run:

async def log_start(ctx: Context, arg: str) -> None:
    log.info(f"starting {arg}")

async def foo(ctx: Context, arg: str) -> str:
    await ctx.run(log_start, arg)         # logged exactly once, persisted
    result = await ctx.run(work, arg)
    return result

Non-determinism regressions

Common non-deterministic code that breaks recovery:

async def bad(ctx: Context, x: str) -> str:
    t = time.time()                        # changes between runs
    r = random.random()                    # changes between runs
    path = os.environ.get("FEATURE")      # changes if env mutates
    if t > 1700000000 or r > 0.5:
        return await ctx.run(branch_a)
    return await ctx.run(branch_b)

Each resumption may hit a different branch than the original run. Wrap non-deterministic values in leaf functions via ctx.run:

async def _get_time(ctx: Context) -> float:
    return time.time()

async def _get_random(ctx: Context) -> float:
    return random.random()

async def good(ctx: Context, x: str) -> str:
    t = await ctx.run(_get_time)
    r = await ctx.run(_get_random)
    # Now t and r are stable across replays (stored at first execution)

Minimal repro templates

Worker:

# worker.py
from __future__ import annotations
import asyncio, os
from typing import TYPE_CHECKING
from resonate.resonate import Resonate

if TYPE_CHECKING:
    from resonate.context import Context

r = Resonate(url=os.environ.get("RESONATE_URL", "http://localhost:8001"), group="workers")

async def ping(ctx: Context, name: str) -> str:
    return f"pong {name}"

r.register(ping)

async def main() -> None:
    try:
        await asyncio.Event().wait()    # block forever; SDK polls for work
    finally:
        await r.stop()

asyncio.run(main())

Client:

# client.py
from __future__ import annotations
import asyncio, os, time
from resonate.resonate import Resonate

async def main() -> None:
    r = Resonate(url=os.environ.get("RESONATE_URL", "http://localhost:8001"))
    try:
        result = await r.options(target="workers").rpc(
            f"ping-{time.time_ns()}", "ping", "alice"
        ).result()
        print(result)
    finally:
        await r.stop()

asyncio.run(main())

If the client hangs, check: worker running? same URL? worker in the right group? resonate promises get <id> → what state is it in?

CLI one-liners

# local dev server — zero config, single port 8001
resonate dev

# inspect a workflow's tree of promises
resonate tree <invocation-id>

# get a specific promise's state
resonate promises get <id>

# resolve/reject external promises from the CLI (e.g., for HITL testing)
resonate promises resolve <id> --data '{"approve": true}'
resonate promises reject <id> --data '{"reason": "denied"}'

# search by prefix or tags
resonate promises search 'order:*'

Related skills

  • resonate-basic-ephemeral-world-usage-python — Client APIs; useful when the failure is at the process-entry layer
  • resonate-basic-durable-world-usage-python — Context APIs; useful when the failure is inside a durable function
  • durable-execution — foundational; the mental model for recovery + replay
  • resonate-philosophy — the "do not build these things" list; many debug sessions end up being about patterns that were already warned against here

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.