Skip to content

Python: executor on_checkpoint_save return value is validated only at restore time: non-dict state checkpoints fine then fails to restore on any storage #8183

Description

@ptimizeroracle

description

_save_executor_states in _runner.py calls executor.on_checkpoint_save() and stores whatever comes back with no shape check. _restore_executor_states then validates isinstance(state, dict) and raises WorkflowCheckpointException("Executor state for {id} is not a dict[str, Any]. Unable to restore.").

so an executor whose on_checkpoint_save returns a list (or any non-dict) checkpoints successfully, the run completes, and every later restore of those checkpoints fails. this is the same save-accepts-what-restore-rejects principle as #8181, but a different locus (the executor state hooks, _runner.py, not the file storage) and it reproduces on InMemoryCheckpointStorage too, so it is not pickle- or storage-specific.

the contract is documented on the Executor base class (on_checkpoint_save() -> dict[str, Any], _executor.py:525), so a returning a non-dict is user error, but the failure surfaces only at restore time, potentially in a different process. a save-side isinstance check (raise WorkflowCheckpointException from _save_executor_states) would make it loud at capture time.

one related observation i am not claiming as a bug: when checkpoint encoding fails mid-run, the run continues by design ("note that this does not fail the workflow run" in the log message). combined with the asymmetry above, an executor returning an unencodable value means the run finishes with no checkpoints at all and only log lines saying so. that may be intended resilience, flagging it only as context.

checked tests (test_checkpoint.py, test_agent_executor.py) and the tracker: nothing pins the non-dict path and no duplicate found. found via a probe of the executor state hooks. ai assistance disclosed. standalone offline repro below.

reproduction steps

import asyncio, sys
sys.path.insert(0, "python/packages/core")
from agent_framework import WorkflowBuilder, WorkflowContext, handler
from agent_framework._workflows._checkpoint import InMemoryCheckpointStorage
from agent_framework._workflows._executor import Executor

class BadStateExecutor(Executor):
    @handler
    async def run(self, message: str, ctx: WorkflowContext) -> None:
        await ctx.yield_output(message + "-done")

    async def on_checkpoint_save(self):
        return ["not", "a", "dict"]  # type: ignore[override]

async def main():
    store = InMemoryCheckpointStorage()
    ex = BadStateExecutor(id="only")
    wf = WorkflowBuilder(max_iterations=5, start_executor=ex, checkpoint_storage=store).build()
    _ = [e async for e in wf.run("hi", stream=True)]          # run completes
    ckpts = await store.list_checkpoints(workflow_name=wf.name)
    print(f"{len(ckpts)} checkpoints saved")                    # checkpoints exist

    ex2 = BadStateExecutor(id="only")
    wf2 = WorkflowBuilder(max_iterations=5, start_executor=ex2, checkpoint_storage=store).build()
    _ = [e async for e in wf2.run(checkpoint_id=ckpts[-1].checkpoint_id, stream=True)]
    # WorkflowCheckpointException: Executor state for only is not a dict[str, Any]. Unable to restore.

asyncio.run(main())

environment

  • agent-framework-core @ main $SHA (editable, python/packages/core)
  • python 3.12, macOS, offline repro

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

pythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflowworkflowsUsage: [Issues, PRs], Target: Workflows

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions