4aa737eee5
* Harden functional workflow continuation authority Use a versioned opaque single-use token on WorkflowRunResult, validate it before request correlation, consume it immediately before replayed user code, and rotate it on each pause. Carry the same explicit authority through streaming and non-streaming FunctionalWorkflowAgent responses. Files changed: functional workflow/runtime result APIs, functional HITL regression tests, core agent guidance, and the functional HITL sample. Next iteration: enforce pending-state overlap and token-authorized abandonment, then document and test checkpoint authorization boundaries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Enforce one pending functional continuation Reject fresh messages and checkpoint restores while an in-memory continuation is pending. Add token-authorized abandonment on FunctionalWorkflow and FunctionalWorkflowAgent, and clear retained replay state atomically when authority is consumed while preserving the active message for token rotation and checkpoints. Files changed: functional workflow runtime and agent adapter, functional lifecycle regression tests, and core workflow guidance. Next iteration: preserve and document authorized checkpoint continuation boundaries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Preserve authorized functional checkpoint continuation Treat checkpoint restore as a host- and storage-authorized path independent of process-local continuation tokens, and issue fresh authority whenever restored execution pauses again. Cover default and per-run storage, deterministic and custom request IDs, token rotation, and checkpoint-plus-response restore. Files changed: functional workflow and checkpoint interface guidance, functional checkpoint lifecycle tests, the functional HITL sample, and core workflow guidance. Next iteration: run the final repository-wide Python validation gates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Validate Python continuation hardening Run the complete Python workspace checks, aggregate coverage suite, repository hooks, and core package build from the final combined worktree. Keep the validation iteration code-neutral because all gates pass without corrective changes. Files changed: none; this commit records the final validation gate. Blockers: none. Next iteration: no remaining AFK tasks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Handle functional checkpoint continuation failures Publish retained continuation state only after checkpoint persistence succeeds, and cover reuse after a transient save failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a8f47743-1cdc-4924-8e1b-667d0d790b78 * Address functional continuation review findings Add owner recovery for lost tokens, harden malformed token validation, preserve consistent failure surfaces, and keep agent pending state aligned with resumable workflow state. Document process-local single-use continuation semantics and extend regression coverage across direct, streaming, checkpoint, and agent paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a8f47743-1cdc-4924-8e1b-667d0d790b78 * Handle functional continuation cancellation Release the workflow run guard when cancellation interrupts resumed user code while keeping the single-use continuation token consumed. Replace sample assertions with explicit runtime checks and add cancellation regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a8f47743-1cdc-4924-8e1b-667d0d790b78 * Simplify functional workflow instance isolation Remove continuation-token handling and align functional workflows with the graph workflow ownership model: one stateful instance per logical caller or session. Add create_instance() for independent callers, document the ownership contract, and cover pending-state isolation between instances. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a8f47743-1cdc-4924-8e1b-667d0d790b78 * Scope functional workflow checkpoint storage Do not inherit checkpoint storage when creating an independent workflow instance. Allow hosts to provide an explicitly caller-scoped storage adapter and document that shared checkpoint access requires host authorization and tenant isolation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a8f47743-1cdc-4924-8e1b-667d0d790b78 * Require building functional workflow instances Make @workflow return a stateless FunctionalWorkflowDefinition and require build() before run() or as_agent(). This aligns functional workflows with the graph definition/build lifecycle and prevents module-level decorated definitions from retaining caller state. Move checkpoint configuration to build(), export the definition type, migrate samples, and cover isolated built instances. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a8f47743-1cdc-4924-8e1b-667d0d790b78 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a8f47743-1cdc-4924-8e1b-667d0d790b78
101 lines
3.7 KiB
Python
101 lines
3.7 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
"""Introducing @step: per-step checkpointing and observability.
|
|
|
|
The previous samples used plain functions — and that works. Workflows support
|
|
HITL (ctx.request_info) and checkpointing regardless of whether you use @step.
|
|
|
|
The difference: without @step, a resumed workflow re-executes every function
|
|
call from the top. That's fine for cheap functions. But for expensive operations
|
|
(API calls, agent runs, etc.) you don't want to pay that cost again.
|
|
|
|
@step saves each function's result so it skips re-execution on resume:
|
|
- On HITL resume, completed steps return their saved result instantly.
|
|
- On crash recovery from a checkpoint, earlier step results are restored.
|
|
- Each step emits executor_invoked/executor_completed events for observability.
|
|
|
|
@step is opt-in. Plain functions still work alongside @step in the same workflow.
|
|
"""
|
|
|
|
import asyncio
|
|
|
|
from agent_framework import InMemoryCheckpointStorage, step, workflow
|
|
|
|
# Track call counts to show which functions actually execute on resume
|
|
fetch_calls = 0
|
|
transform_calls = 0
|
|
|
|
|
|
# @step saves this function's result. On resume, it returns the saved
|
|
# result instead of re-executing — useful because this is expensive.
|
|
@step
|
|
async def fetch_data(url: str) -> dict[str, str | int]:
|
|
"""Expensive operation — @step prevents re-execution on resume."""
|
|
global fetch_calls
|
|
fetch_calls += 1
|
|
print(f" fetch_data called (call #{fetch_calls})")
|
|
return {"url": url, "content": f"Data from {url}", "status": 200}
|
|
|
|
|
|
@step
|
|
async def transform_data(data: dict[str, str | int]) -> str:
|
|
"""Another expensive operation — @step saves the result."""
|
|
global transform_calls
|
|
transform_calls += 1
|
|
print(f" transform_data called (call #{transform_calls})")
|
|
return f"[{data['status']}] {data['content']}"
|
|
|
|
|
|
# No @step — this is cheap, so it just re-runs on resume. That's fine.
|
|
async def validate_result(summary: str) -> bool:
|
|
"""Cheap validation — no @step needed."""
|
|
return len(summary) > 0 and "[200]" in summary
|
|
|
|
|
|
storage = InMemoryCheckpointStorage()
|
|
|
|
|
|
# Build with checkpoint_storage to persist step results.
|
|
# Each @step saves a checkpoint after it completes.
|
|
@workflow
|
|
async def data_pipeline(url: str) -> str:
|
|
"""Mix of @step functions and plain functions."""
|
|
raw = await fetch_data(url)
|
|
summary = await transform_data(raw)
|
|
is_valid = await validate_result(summary)
|
|
|
|
return f"{summary} (valid={is_valid})"
|
|
|
|
|
|
async def main():
|
|
workflow_instance = data_pipeline.build(checkpoint_storage=storage)
|
|
|
|
# --- Run 1: Everything executes normally ---
|
|
print("=== Run 1: Fresh execution ===")
|
|
result = await workflow_instance.run("https://example.com/api/data")
|
|
print(f"Output: {result.get_outputs()[0]}")
|
|
print(f"fetch_calls={fetch_calls}, transform_calls={transform_calls}")
|
|
|
|
# @step functions emit executor events; plain functions don't.
|
|
print("\nEvents:")
|
|
for event in result:
|
|
if event.type in ("executor_invoked", "executor_completed"):
|
|
print(f" {event.type}: {event.executor_id}")
|
|
|
|
# --- Run 2: Restore from checkpoint ---
|
|
# The workflow re-executes, but @step functions return saved results.
|
|
# Only validate_result() (no @step) actually runs again.
|
|
print("\n=== Run 2: Restored from checkpoint ===")
|
|
latest = await storage.get_latest(workflow_name="data_pipeline")
|
|
if latest is None:
|
|
raise RuntimeError("Expected a checkpoint from the first run.")
|
|
|
|
result2 = await workflow_instance.run(checkpoint_id=latest.checkpoint_id)
|
|
print(f"Output: {result2.get_outputs()[0]}")
|
|
print(f"fetch_calls={fetch_calls}, transform_calls={transform_calls}")
|
|
print("(call counts unchanged — @step results were restored from checkpoint)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|