fix(wac): return the checkpointed value from step(), not the live object (#10367)

* fix(wac): return the checkpointed value from step(), not the live object

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(wac): regenerate system prompts and narrow the round-trip claim

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style(wac): condense the round-trip comments and fix the fallback note

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(sdk): type step() as the JSON round trip of its body's result

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(sdk): apply the JSON round trip to task() and the standalone paths

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sdk): encode bigint, keep unknown as unknown, align dropped-key results

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(wac): null out results whose key JSON.stringify would drop

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(wac): normalize only the top-level result, keeping nested keys as they were

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(wac): normalize a child task's result so a deployed job cannot fail to parse

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(sdk): pin non-finite number behavior in Jsonified and its tests

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sdk): admit undefined for keys whose value JSON.stringify may omit

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sdk): make a key JSON.stringify may omit optional, not just nullable

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(sdk): treat a class-valued property as dropped, like any other function

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-07-28 09:46:23 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent b0c7e09173
commit 044ce39e5f
17 changed files with 695 additions and 65 deletions
+57 -5
View File
@@ -1004,7 +1004,15 @@ setWorkflowCtx(ctx: WorkflowCtx | null): void
async sleep(seconds: number): Promise<void>
async step<T>(name: string, fn: () => T | Promise<T>): Promise<T>
/**
* Execute \`fn\` inline and checkpoint the result. On replay the cached value is
* returned without re-executing \`fn\`.
*
* \`fn\`'s result is encoded as JSON and decoded back before it is returned, so
* the round that runs the body sees the same types every replay sees: a \`Date\`
* comes back as a string, a \`Map\` as \`{}\`. {@link Jsonified} is that shape.
*/
async step<T>(name: string, fn: () => T | Promise<T>,): Promise<Jsonified<Awaited<T>>>
/**
* Create a task that dispatches to a separate Windmill script.
@@ -1796,7 +1804,15 @@ setWorkflowCtx(ctx: WorkflowCtx | null): void
async sleep(seconds: number): Promise<void>
async step<T>(name: string, fn: () => T | Promise<T>): Promise<T>
/**
* Execute \`fn\` inline and checkpoint the result. On replay the cached value is
* returned without re-executing \`fn\`.
*
* \`fn\`'s result is encoded as JSON and decoded back before it is returned, so
* the round that runs the body sees the same types every replay sees: a \`Date\`
* comes back as a string, a \`Map\` as \`{}\`. {@link Jsonified} is that shape.
*/
async step<T>(name: string, fn: () => T | Promise<T>,): Promise<Jsonified<Awaited<T>>>
/**
* Create a task that dispatches to a separate Windmill script.
@@ -2682,7 +2698,15 @@ setWorkflowCtx(ctx: WorkflowCtx | null): void
async sleep(seconds: number): Promise<void>
async step<T>(name: string, fn: () => T | Promise<T>): Promise<T>
/**
* Execute \`fn\` inline and checkpoint the result. On replay the cached value is
* returned without re-executing \`fn\`.
*
* \`fn\`'s result is encoded as JSON and decoded back before it is returned, so
* the round that runs the body sees the same types every replay sees: a \`Date\`
* comes back as a string, a \`Map\` as \`{}\`. {@link Jsonified} is that shape.
*/
async step<T>(name: string, fn: () => T | Promise<T>,): Promise<Jsonified<Awaited<T>>>
/**
* Create a task that dispatches to a separate Windmill script.
@@ -4570,6 +4594,10 @@ def parse_sql_client_name(name: str) -> tuple[str, Optional[str]]
# - **v1 (WM_JOB_ID set, no @workflow)**: dispatches via HTTP API.
# - **Standalone**: executes the function body directly.
#
# A task runs as its own job, so its result is always encoded as JSON and
# decoded back before the caller sees it: a \`\`datetime\`\` comes back as a
# string, a tuple as a list.
#
# Usage::
#
# @task
@@ -4615,6 +4643,10 @@ def workflow(func)
# On replay the cached value is returned without re-executing \`\`fn\`\`.
# Use for lightweight deterministic operations (timestamps, random IDs,
# config reads) that should not incur the overhead of a child job.
#
# \`\`fn\`\`'s result is encoded as JSON and decoded back before it is returned,
# so the round that runs the body sees the same types every replay sees:
# a \`\`datetime\`\` comes back as a string, a tuple as a list.
async def step(name: str, fn)
# Server-side sleep suspend the workflow for the given duration without holding a worker.
@@ -6508,8 +6540,12 @@ export async function getResumeUrls(approver?: string, flowLevel?: boolean): Pro
*
* Inside a \`workflow()\`, calling a task dispatches it as a step.
* Outside a workflow, the function body executes directly.
*
* A task runs as its own job, so its result is always encoded as JSON and
* decoded back before the caller sees it: a \`Date\` comes back as a string, a
* \`Map\` as \`{}\`. {@link JsonifiedFn} is that shape.
*/
export function task<T extends (...args: any[]) => Promise<any>>(fnOrPath: T | string, maybeFnOrOptions?: T | TaskOptions, maybeOptions?: TaskOptions,): T
export function task<T extends (...args: any[]) => Promise<any>>(fnOrPath: T | string, maybeFnOrOptions?: T | TaskOptions, maybeOptions?: TaskOptions,): JsonifiedFn<T>
/**
* Create a task that dispatches to a separate Windmill script.
@@ -6540,7 +6576,15 @@ export function taskFlow(path: string, options?: TaskOptions): (...args: any[])
*/
export function workflow<T>(fn: (...args: any[]) => Promise<T>)
export async function step<T>(name: string, fn: () => T | Promise<T>): Promise<T>
/**
* Execute \`fn\` inline and checkpoint the result. On replay the cached value is
* returned without re-executing \`fn\`.
*
* \`fn\`'s result is encoded as JSON and decoded back before it is returned, so
* the round that runs the body sees the same types every replay sees: a \`Date\`
* comes back as a string, a \`Map\` as \`{}\`. {@link Jsonified} is that shape.
*/
export async function step<T>(name: string, fn: () => T | Promise<T>,): Promise<Jsonified<Awaited<T>>>
export async function sleep(seconds: number): Promise<void>
@@ -6630,6 +6674,10 @@ def get_resume_urls(approver: str = None, flow_level: bool = None) -> dict
# - **v1 (WM_JOB_ID set, no @workflow)**: dispatches via HTTP API.
# - **Standalone**: executes the function body directly.
#
# A task runs as its own job, so its result is always encoded as JSON and
# decoded back before the caller sees it: a \`\`datetime\`\` comes back as a
# string, a tuple as a list.
#
# Usage::
#
# @task
@@ -6675,6 +6723,10 @@ def workflow(func)
# On replay the cached value is returned without re-executing \`\`fn\`\`.
# Use for lightweight deterministic operations (timestamps, random IDs,
# config reads) that should not incur the overhead of a child job.
#
# \`\`fn\`\`'s result is encoded as JSON and decoded back before it is returned,
# so the round that runs the body sees the same types every replay sees:
# a \`\`datetime\`\` comes back as a string, a tuple as a list.
async def step(name: str, fn)
# Server-side sleep suspend the workflow for the given duration without holding a worker.