Skip to content

Attempt to add memo functions to durable context for TS and Py SDKs#2921

Draft
mnafees wants to merge 6 commits intonafees/go-sdk-memofrom
nafees/ts-py-sdks-memo
Draft

Attempt to add memo functions to durable context for TS and Py SDKs#2921
mnafees wants to merge 6 commits intonafees/go-sdk-memofrom
nafees/ts-py-sdks-memo

Conversation

@mnafees
Copy link
Member

@mnafees mnafees commented Feb 3, 2026

Description

Fixes # (issue)

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Documentation change (pure documentation change)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (non-breaking changes to code which doesn't change any behaviour)
  • CI (any automation pipeline changes)
  • Chore (changes which are not directly related to any business logic)
  • Test changes (add, refactor, improve or change a test)
  • This change requires a documentation update

What's Changed

  • Add a list of tasks or features here...

@mnafees mnafees self-assigned this Feb 3, 2026
@vercel
Copy link

vercel bot commented Feb 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hatchet-docs Ready Ready Preview, Comment Feb 4, 2026 6:11pm

Request Review

Comment on lines 517 to 519
async def aio_memo(
self, fn: Callable[[], Awaitable[Any]], deps: list[Any]
) -> Any:
Copy link
Contributor

Choose a reason for hiding this comment

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

this is going to be a bit of a tricky one to get right, I think 🤔. we probably want both memo and aio_memo, where memo takes a regular sync function and aio_memo takes an async one. In this case, we should have some generic like TMemo = TypeVar("TMemo"), and then have this signature be:

    async def aio_memo(
        self, fn: Callable[[], Awaitable[TMemo]], deps: list[Any]
    ) -> TMemo:

and then for sync memo, it'd be:

    def memo(
        self, fn: Callable[[], TMemo], deps: list[Any]
    ) -> TMemo:


key = _compute_memo_key(self.action.action_id, deps)

resp = self.durable_event_listener.get_durable_event_log(
Copy link
Contributor

Choose a reason for hiding this comment

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

this needs to be wrapped in asyncio.to_thread so it doesn't block

)

if resp.found:
return json.loads(resp.data)
Copy link
Contributor

Choose a reason for hiding this comment

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

to_thread here too

Comment on lines +535 to +541
data = json.dumps(result).encode()

self.durable_event_listener.create_durable_event_log(
external_id=self.workflow_run_id,
key=key,
data=data,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

and to_thread for these ones :)

def _compute_memo_key(step_name: str, deps: list[Any]) -> str:
h = hashlib.sha256()
h.update(step_name.encode())
h.update(json.dumps(deps).encode())
Copy link
Contributor

Choose a reason for hiding this comment

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

this seems a bit risky to me because people can put in whatever they want, but I'm not sure how better to do it. we might want json.dumps(deps, default=str) just in case to try to avoid errors from things that aren't serializable


if resp.found:
return json.loads(resp.data)
return json.loads(resp.data) # type: ignore[no-any-return]
Copy link
Contributor

Choose a reason for hiding this comment

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

it's a small thing, but I have a slight preference for this (we do it elsewhere) because it's clearer when reading the code what's going on:

return cast(TMemo, json.loads(resp.data))


result = await fn()

data = json.dumps(result).encode()
Copy link
Contributor

Choose a reason for hiding this comment

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

to_thread for the json.dumps too :P

one thing I'm curious about here too is what happens if the function e.g. returns a pydantic model or a dataclass or something we can't call json.dumps on. I think we probably should support dataclasses, Pydantic, etc. but it makes the (de)serialization and typing trickier here, but it also makes the DX much better

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