chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,330 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
import sys
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any
from agent_framework import (
Agent,
AgentExecutor,
AgentExecutorRequest,
AgentExecutorResponse,
Executor,
FileCheckpointStorage,
Message,
Workflow,
WorkflowBuilder,
WorkflowContext,
handler,
response_handler,
)
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
from typing_extensions import override # type: ignore[import] # pragma: no cover
# Load environment variables from .env file
load_dotenv()
"""
Sample: Checkpoint + human-in-the-loop quickstart.
This getting-started sample keeps the moving pieces to a minimum:
1. A brief is turned into a consistent prompt for an AI copywriter.
2. The copywriter (an `AgentExecutor`) drafts release notes.
3. A reviewer gateway sends a request for approval for every draft.
4. The workflow records checkpoints between each superstep so you can stop the
program, restart later, and optionally pre-supply human answers on resume.
Key concepts demonstrated
-------------------------
- Minimal executor pipeline with checkpoint persistence.
- Human-in-the-loop pause/resume with checkpoint restoration.
Typical pause/resume flow
-------------------------
1. Run the workflow until a human approval request is emitted.
2. If the human is offline, exit the program. A checkpoint with
``status=awaiting human response`` now exists.
3. Later, restart the script, select that checkpoint, and provide the stored
human decision when prompted to pre-supply responses.
Doing so applies the answer immediately on resume, so the system does **not**
re-emit the same ``.
"""
# Directory used for the sample's temporary checkpoint files. We isolate the
# demo artefacts so that repeated runs do not collide with other samples and so
# the clean-up step at the end of the script can simply delete the directory.
TEMP_DIR = Path(__file__).with_suffix("").parent / "tmp" / "checkpoints_hitl"
TEMP_DIR.mkdir(parents=True, exist_ok=True)
class BriefPreparer(Executor):
"""Normalises the user brief and sends a single AgentExecutorRequest."""
# The first executor in the workflow. By keeping it tiny we make it easier
# to reason about the state that will later be captured in the checkpoint.
# It is responsible for tidying the human-provided brief and kicking off the
# agent run with a deterministic prompt structure.
def __init__(self, id: str, agent_id: str) -> None:
super().__init__(id=id)
self._agent_id = agent_id
@handler
async def prepare(self, brief: str, ctx: WorkflowContext[AgentExecutorRequest, str]) -> None:
# Collapse errant whitespace so the prompt is stable between runs.
normalized = " ".join(brief.split()).strip()
if not normalized.endswith("."):
normalized += "."
# Persist the cleaned brief in workflow state so downstream executors and
# future checkpoints can recover the original intent.
ctx.set_state("brief", normalized)
prompt = (
"You are drafting product release notes. Summarise the brief below in two sentences. "
"Keep it positive and end with a call to action.\n\n"
f"BRIEF: {normalized}"
)
# Hand the prompt to the writer agent. We always route through the
# workflow context so the runtime can capture messages for checkpointing.
await ctx.send_message(
AgentExecutorRequest(messages=[Message("user", contents=[prompt])], should_respond=True),
target_id=self._agent_id,
)
@dataclass
class HumanApprovalRequest:
"""Request sent to the human reviewer."""
# These fields are intentionally simple because they are serialised into
# checkpoints. Keeping them primitive types guarantees the new
# `pending_requests_from_checkpoint` helper can reconstruct them on resume.
prompt: str = ""
draft: str = ""
iteration: int = 0
class ReviewGateway(Executor):
"""Routes agent drafts to humans and optionally back for revisions."""
def __init__(self, id: str, writer_id: str) -> None:
super().__init__(id=id)
self._writer_id = writer_id
self._iteration = 0
@handler
async def on_agent_response(self, response: AgentExecutorResponse, ctx: WorkflowContext) -> None:
# Capture the agent output so we can surface it to the reviewer and persist iterations.
self._iteration += 1
# Emit a human approval request.
await ctx.request_info(
request_data=HumanApprovalRequest(
prompt="Review the draft. Reply 'approve' or provide edit instructions.",
draft=response.agent_response.text,
iteration=self._iteration,
),
response_type=str,
)
@response_handler
async def on_human_feedback(
self,
original_request: HumanApprovalRequest,
feedback: str,
ctx: WorkflowContext[AgentExecutorRequest | str, str],
) -> None:
# The `original_request` is the request we sent earlier that is now being answered.
reply = feedback.strip()
if len(reply) == 0 or reply.lower() == "approve":
# Workflow is completed when the human approves.
await ctx.yield_output(original_request.draft)
return
# Any other response loops us back to the writer with fresh guidance.
prompt = (
"Revise the launch note. Respond with the new copy only.\n\n"
f"Previous draft:\n{original_request.draft}\n\n"
f"Human guidance: {reply}"
)
await ctx.send_message(
AgentExecutorRequest(messages=[Message("user", contents=[prompt])], should_respond=True),
target_id=self._writer_id,
)
@override
async def on_checkpoint_save(self) -> dict[str, Any]:
# Save the current iteration count in executor state for checkpointing.
return {"iteration": self._iteration}
@override
async def on_checkpoint_restore(self, state: dict[str, Any]) -> None:
# Restore the iteration count from executor state during checkpoint recovery.
self._iteration = state.get("iteration", 0)
def create_workflow(checkpoint_storage: FileCheckpointStorage) -> Workflow:
"""Assemble the workflow graph used by both the initial run and resume."""
# Wire the workflow DAG. Edges mirror the numbered steps described in the
# module docstring. Because `WorkflowBuilder` is declarative, reading these
# edges is often the quickest way to understand execution order.
writer_agent = Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
instructions="Write concise, warm release notes that sound human and helpful.",
name="writer",
)
writer = AgentExecutor(writer_agent)
review_gateway = ReviewGateway(id="review_gateway", writer_id="writer")
prepare_brief = BriefPreparer(id="prepare_brief", agent_id="writer")
workflow_builder = (
WorkflowBuilder(max_iterations=6, start_executor=prepare_brief, checkpoint_storage=checkpoint_storage)
.add_edge(prepare_brief, writer)
.add_edge(writer, review_gateway)
.add_edge(review_gateway, writer) # revisions loop
)
return workflow_builder.build()
def prompt_for_responses(requests: dict[str, HumanApprovalRequest]) -> dict[str, str]:
"""Interactive CLI prompt for any live RequestInfo requests."""
responses: dict[str, str] = {}
for request_id, request in requests.items():
print("\n=== Human approval needed ===")
print(f"request_id: {request_id}")
print(f"Iteration: {request.iteration}")
print(request.prompt)
print("Draft: \n---\n" + request.draft + "\n---")
response = input("Type 'approve' or enter revision guidance (or 'exit' to quit): ").strip()
if response.lower() == "exit":
raise SystemExit("Stopped by user.")
responses[request_id] = response
return responses
async def run_interactive_session(
workflow: Workflow,
initial_message: str | None = None,
checkpoint_id: str | None = None,
) -> str:
"""Run the workflow until it either finishes or pauses for human input."""
requests: dict[str, HumanApprovalRequest] = {}
responses: dict[str, str] | None = None
completed_output: str | None = None
while True:
if responses:
event_stream = workflow.run(stream=True, responses=responses)
requests.clear()
responses = None
else:
if initial_message:
print(f"\nStarting workflow with brief: {initial_message}\n")
event_stream = workflow.run(message=initial_message, stream=True)
elif checkpoint_id:
print("\nStarting workflow from checkpoint...\n")
event_stream = workflow.run(checkpoint_id=checkpoint_id, stream=True)
else:
raise ValueError("Either initial_message or checkpoint_id must be provided")
async for event in event_stream:
if event.type == "status":
print(event)
if event.type == "output":
completed_output = event.data
if event.type == "request_info":
if isinstance(event.data, HumanApprovalRequest):
requests[event.request_id] = event.data
else:
raise ValueError("Unexpected request data type")
if completed_output:
break
if requests:
responses = prompt_for_responses(requests)
continue
raise RuntimeError("Workflow stopped without completing or requesting input")
return completed_output
async def main() -> None:
"""Entry point used by both the initial run and subsequent resumes."""
for file in TEMP_DIR.glob("*.json"):
# Start each execution with a clean slate so the demonstration is
# deterministic even if the directory had stale checkpoints.
file.unlink()
storage = FileCheckpointStorage(storage_path=TEMP_DIR)
workflow = create_workflow(checkpoint_storage=storage)
brief = (
"Introduce our limited edition smart coffee grinder. Mention the $249 price, highlight the "
"sensor that auto-adjusts the grind, and invite customers to pre-order on the website."
)
print("Running workflow (human approval required)...")
result = await run_interactive_session(workflow, initial_message=brief)
print(f"Workflow completed with: {result}")
checkpoints = await storage.list_checkpoints(workflow_name=workflow.name)
if not checkpoints:
print("No checkpoints recorded.")
return
sorted_cps = sorted(checkpoints, key=lambda cp: datetime.fromisoformat(cp.timestamp))
print("\nAvailable checkpoints:")
for idx, cp in enumerate(sorted_cps):
print(f" [{idx}] id={cp.checkpoint_id} iter={cp.iteration_count}")
# For the pause/resume demo we typically pick the latest checkpoint whose summary
# status reads "awaiting human response" - that is the saved state that proves the
# workflow can rehydrate, collect the pending answer, and continue after a break.
selection = input("\nResume from which checkpoint? (press Enter to skip): ").strip() # noqa: ASYNC250
if not selection:
print("No resume selected. Exiting.")
return
try:
idx = int(selection)
except ValueError:
print("Invalid input; exiting.")
return
if not 0 <= idx < len(sorted_cps):
print("Index out of range; exiting.")
return
chosen = sorted_cps[idx]
new_workflow = create_workflow(checkpoint_storage=storage)
# Resume with a fresh workflow instance. The checkpoint carries the
# persistent state while this object holds the runtime wiring.
result = await run_interactive_session(new_workflow, checkpoint_id=chosen.checkpoint_id)
print(f"Workflow completed with: {result}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,157 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Sample: Checkpointing and Resuming a Workflow
Purpose:
This sample shows how to enable checkpointing for a long-running workflow
that can be paused and resumed.
What you learn:
- How to configure checkpointing storage (InMemoryCheckpointStorage for testing)
- How to resume a workflow from a checkpoint after interruption
- How to implement executor state management with checkpoint hooks
- How to handle workflow interruptions and automatic recovery
Pipeline:
This sample shows a workflow that computes factor pairs for numbers up to a given limit:
1) A start executor that receives the upper limit and creates the initial task
2) A worker executor that processes each number to find its factor pairs
3) The worker uses checkpoint hooks to save/restore its internal state
Prerequisites:
- Basic understanding of workflow concepts, including executors, edges, events, etc.
"""
import asyncio
import sys
from dataclasses import dataclass
from random import random
from typing import Any
from agent_framework import (
Executor,
InMemoryCheckpointStorage,
WorkflowBuilder,
WorkflowCheckpoint,
WorkflowContext,
handler,
)
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
from typing_extensions import override # type: ignore[import] # pragma: no cover
@dataclass
class ComputeTask:
"""Task containing the list of numbers remaining to be processed."""
remaining_numbers: list[int]
class StartExecutor(Executor):
"""Initiates the workflow by providing the upper limit for factor pair computation."""
@handler
async def start(self, upper_limit: int, ctx: WorkflowContext[ComputeTask]) -> None:
"""Start the workflow with a list of numbers to process."""
print(f"StartExecutor: Starting factor pair computation up to {upper_limit}")
await ctx.send_message(ComputeTask(remaining_numbers=list(range(1, upper_limit + 1))))
class WorkerExecutor(Executor):
"""Processes numbers to compute their factor pairs and manages executor state for checkpointing."""
def __init__(self, id: str) -> None:
super().__init__(id=id)
self._composite_number_pairs: dict[int, list[tuple[int, int]]] = {}
@handler
async def compute(
self,
task: ComputeTask,
ctx: WorkflowContext[ComputeTask, dict[int, list[tuple[int, int]]]],
) -> None:
"""Process the next number in the task, computing its factor pairs."""
next_number = task.remaining_numbers.pop(0)
print(f"WorkerExecutor: Computing factor pairs for {next_number}")
pairs: list[tuple[int, int]] = []
for i in range(1, next_number):
if next_number % i == 0:
pairs.append((i, next_number // i))
self._composite_number_pairs[next_number] = pairs
if not task.remaining_numbers:
# All numbers processed - output the results
await ctx.yield_output(self._composite_number_pairs)
else:
# More numbers to process - continue with remaining task
await ctx.send_message(task)
@override
async def on_checkpoint_save(self) -> dict[str, Any]:
"""Save the executor's internal state for checkpointing."""
return {"composite_number_pairs": self._composite_number_pairs}
@override
async def on_checkpoint_restore(self, state: dict[str, Any]) -> None:
"""Restore the executor's internal state from a checkpoint."""
self._composite_number_pairs = state.get("composite_number_pairs", {})
async def main():
# Build workflow with checkpointing enabled
checkpoint_storage = InMemoryCheckpointStorage()
start = StartExecutor(id="start")
worker = WorkerExecutor(id="worker")
workflow_builder = (
WorkflowBuilder(start_executor=start, checkpoint_storage=checkpoint_storage)
.add_edge(start, worker)
.add_edge(worker, worker) # Self-loop for iterative processing
)
# Run workflow with automatic checkpoint recovery
latest_checkpoint: WorkflowCheckpoint | None = None
while True:
workflow = workflow_builder.build()
# Start from checkpoint or fresh execution
print(f"\n** Workflow {workflow.id} started **")
event_stream = (
workflow.run(message=10, stream=True)
if latest_checkpoint is None
else workflow.run(checkpoint_id=latest_checkpoint.checkpoint_id, stream=True)
)
output: str | None = None
async for event in event_stream:
if event.type == "output":
output = event.data
break
if event.type == "superstep_completed" and random() < 0.5:
# Randomly simulate system interruptions
# The type="superstep_completed" event ensures we only interrupt after
# the current super-step is fully complete and checkpointed.
# If we interrupt mid-step, the workflow may resume from an earlier point.
print("\n** Simulating workflow interruption. Stopping execution. **")
break
# Find the latest checkpoint to resume from
latest_checkpoint = await checkpoint_storage.get_latest(workflow_name=workflow.name)
if not latest_checkpoint:
raise RuntimeError("No checkpoints available to resume from.")
print(
f"Checkpoint {latest_checkpoint.checkpoint_id}: "
f"(iter={latest_checkpoint.iteration_count}, messages={latest_checkpoint.messages})"
)
if output is not None:
print(f"\nWorkflow completed successfully with output: {output}")
break
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,201 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: T201
"""Sample: Workflow Checkpointing with Cosmos DB NoSQL.
Purpose:
This sample shows how to use Azure Cosmos DB NoSQL as a persistent checkpoint
storage backend for workflows, enabling durable pause-and-resume across
process restarts.
What you learn:
- How to configure CosmosCheckpointStorage for workflow checkpointing
- How to run a workflow that automatically persists checkpoints to Cosmos DB
- How to resume a workflow from a Cosmos DB checkpoint
- How to list and inspect available checkpoints
Prerequisites:
- An Azure Cosmos DB account (or local emulator)
- Environment variables set (see below)
Environment variables:
AZURE_COSMOS_ENDPOINT - Cosmos DB account endpoint
AZURE_COSMOS_DATABASE_NAME - Database name
AZURE_COSMOS_CONTAINER_NAME - Container name for checkpoints
Optional:
AZURE_COSMOS_KEY - Account key (if not using Azure credentials)
"""
import asyncio
import os
import sys
from dataclasses import dataclass
from typing import Any
from agent_framework import (
Executor,
WorkflowBuilder,
WorkflowCheckpoint,
WorkflowContext,
handler,
)
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
from typing_extensions import override # type: ignore[import] # pragma: no cover
from agent_framework_azure_cosmos import CosmosCheckpointStorage
@dataclass
class ComputeTask:
"""Task containing the list of numbers remaining to be processed."""
remaining_numbers: list[int]
class StartExecutor(Executor):
"""Initiates the workflow by providing the upper limit."""
@handler
async def start(self, upper_limit: int, ctx: WorkflowContext[ComputeTask]) -> None:
"""Start the workflow with numbers up to the given limit."""
print(f"StartExecutor: Starting computation up to {upper_limit}")
await ctx.send_message(ComputeTask(remaining_numbers=list(range(1, upper_limit + 1))))
class WorkerExecutor(Executor):
"""Processes numbers and manages executor state for checkpointing."""
def __init__(self, id: str) -> None:
"""Initialize the worker executor."""
super().__init__(id=id)
self._results: dict[int, list[tuple[int, int]]] = {}
@handler
async def compute(
self,
task: ComputeTask,
ctx: WorkflowContext[ComputeTask, dict[int, list[tuple[int, int]]]],
) -> None:
"""Process the next number, computing its factor pairs."""
next_number = task.remaining_numbers.pop(0)
print(f"WorkerExecutor: Processing {next_number}")
pairs: list[tuple[int, int]] = []
for i in range(1, next_number):
if next_number % i == 0:
pairs.append((i, next_number // i))
self._results[next_number] = pairs
if not task.remaining_numbers:
await ctx.yield_output(self._results)
else:
await ctx.send_message(task)
@override
async def on_checkpoint_save(self) -> dict[str, Any]:
return {"results": self._results}
@override
async def on_checkpoint_restore(self, state: dict[str, Any]) -> None:
self._results = state.get("results", {})
async def main() -> None:
"""Run the workflow checkpointing sample with Cosmos DB."""
cosmos_endpoint = os.getenv("AZURE_COSMOS_ENDPOINT")
cosmos_database_name = os.getenv("AZURE_COSMOS_DATABASE_NAME")
cosmos_container_name = os.getenv("AZURE_COSMOS_CONTAINER_NAME")
cosmos_key = os.getenv("AZURE_COSMOS_KEY")
if not cosmos_endpoint or not cosmos_database_name or not cosmos_container_name:
print("Please set AZURE_COSMOS_ENDPOINT, AZURE_COSMOS_DATABASE_NAME, and AZURE_COSMOS_CONTAINER_NAME.")
return
# Authentication: supports both managed identity/RBAC and key-based auth.
# When AZURE_COSMOS_KEY is set, key-based auth is used.
# Otherwise, falls back to DefaultAzureCredential (properly closed via async with).
if cosmos_key:
async with CosmosCheckpointStorage(
endpoint=cosmos_endpoint,
credential=cosmos_key,
database_name=cosmos_database_name,
container_name=cosmos_container_name,
) as checkpoint_storage:
await _run_workflow(checkpoint_storage)
else:
from azure.identity.aio import DefaultAzureCredential
async with (
DefaultAzureCredential() as credential,
CosmosCheckpointStorage(
endpoint=cosmos_endpoint,
credential=credential,
database_name=cosmos_database_name,
container_name=cosmos_container_name,
) as checkpoint_storage,
):
await _run_workflow(checkpoint_storage)
async def _run_workflow(checkpoint_storage: CosmosCheckpointStorage) -> None:
"""Build and run the workflow with Cosmos DB checkpointing."""
start = StartExecutor(id="start")
worker = WorkerExecutor(id="worker")
workflow_builder = (
WorkflowBuilder(start_executor=start, checkpoint_storage=checkpoint_storage)
.add_edge(start, worker)
.add_edge(worker, worker)
)
# --- First run: execute the workflow ---
print("\n=== First Run ===\n")
workflow = workflow_builder.build()
output = None
async for event in workflow.run(message=8, stream=True):
if event.type == "output":
output = event.data
print(f"Factor pairs computed: {output}")
# List checkpoints saved in Cosmos DB
checkpoint_ids = await checkpoint_storage.list_checkpoint_ids(
workflow_name=workflow.name,
)
print(f"\nCheckpoints in Cosmos DB: {len(checkpoint_ids)}")
for cid in checkpoint_ids:
print(f" - {cid}")
# Get the latest checkpoint
latest: WorkflowCheckpoint | None = await checkpoint_storage.get_latest(
workflow_name=workflow.name,
)
if latest is None:
print("No checkpoint found to resume from.")
return
print(f"\nLatest checkpoint: {latest.checkpoint_id}")
print(f" iteration_count: {latest.iteration_count}")
print(f" timestamp: {latest.timestamp}")
# --- Second run: resume from the latest checkpoint ---
print("\n=== Resuming from Checkpoint ===\n")
workflow2 = workflow_builder.build()
output2 = None
async for event in workflow2.run(checkpoint_id=latest.checkpoint_id, stream=True):
if event.type == "output":
output2 = event.data
if output2:
print(f"Resumed workflow produced: {output2}")
else:
print("Resumed workflow completed (no remaining work — already finished).")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,141 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: T201
"""Sample: Workflow Checkpointing with Cosmos DB and Azure AI Foundry.
Purpose:
This sample demonstrates how to use CosmosCheckpointStorage with agents built
on Azure AI Foundry (via FoundryChatClient). It shows a multi-agent
workflow where checkpoint state is persisted to Cosmos DB, enabling durable
pause-and-resume across process restarts.
What you learn:
- How to wire CosmosCheckpointStorage with FoundryChatClient agents
- How to combine session history with workflow checkpointing
- How to resume a workflow-as-agent from a Cosmos DB checkpoint
Key concepts:
- AgentSession: Maintains conversation history across agent invocations
- CosmosCheckpointStorage: Persists workflow execution state in Cosmos DB
- These are complementary: sessions track conversation, checkpoints track workflow state
Environment variables:
FOUNDRY_PROJECT_ENDPOINT - Azure AI Foundry project endpoint
FOUNDRY_MODEL - Model deployment name
AZURE_COSMOS_ENDPOINT - Cosmos DB account endpoint
AZURE_COSMOS_DATABASE_NAME - Database name
AZURE_COSMOS_CONTAINER_NAME - Container name for checkpoints
Optional:
AZURE_COSMOS_KEY - Account key (if not using Azure credentials)
"""
import asyncio
import os
from typing import Any
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import SequentialBuilder
from agent_framework_azure_cosmos import CosmosCheckpointStorage
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
load_dotenv()
async def main() -> None:
"""Run the Azure AI Foundry + Cosmos DB checkpointing sample."""
project_endpoint = os.getenv("FOUNDRY_PROJECT_ENDPOINT")
model = os.getenv("FOUNDRY_MODEL")
cosmos_endpoint = os.getenv("AZURE_COSMOS_ENDPOINT")
cosmos_database_name = os.getenv("AZURE_COSMOS_DATABASE_NAME")
cosmos_container_name = os.getenv("AZURE_COSMOS_CONTAINER_NAME")
cosmos_key = os.getenv("AZURE_COSMOS_KEY")
if not project_endpoint or not model:
print("Please set FOUNDRY_PROJECT_ENDPOINT and FOUNDRY_MODEL.")
return
if not cosmos_endpoint or not cosmos_database_name or not cosmos_container_name:
print("Please set AZURE_COSMOS_ENDPOINT, AZURE_COSMOS_DATABASE_NAME, and AZURE_COSMOS_CONTAINER_NAME.")
return
# Use a single AzureCliCredential for both Cosmos and Foundry,
# properly closed via async context manager.
async with AzureCliCredential() as azure_credential:
cosmos_credential: Any = cosmos_key if cosmos_key else azure_credential
async with CosmosCheckpointStorage(
endpoint=cosmos_endpoint,
credential=cosmos_credential,
database_name=cosmos_database_name,
container_name=cosmos_container_name,
) as checkpoint_storage:
# Create Azure AI Foundry agents
client = FoundryChatClient(
project_endpoint=project_endpoint,
model=model,
credential=azure_credential,
)
assistant = Agent(
name="assistant",
instructions="You are a helpful assistant. Keep responses brief.",
client=client,
)
reviewer = Agent(
name="reviewer",
instructions="You are a reviewer. Provide a one-sentence summary of the assistant's response.",
client=client,
)
# Build a sequential workflow and wrap it as an agent
workflow = SequentialBuilder(participants=[assistant, reviewer]).build()
agent = workflow.as_agent(name="FoundryCheckpointedAgent")
# --- First run: execute with Cosmos DB checkpointing ---
print("=== First Run ===\n")
session = agent.create_session()
query = "What are the benefits of renewable energy?"
print(f"User: {query}")
response = await agent.run(query, session=session, checkpoint_storage=checkpoint_storage)
for msg in response.messages:
speaker = msg.author_name or msg.role
print(f"[{speaker}]: {msg.text}")
# Show checkpoints persisted in Cosmos DB
checkpoints = await checkpoint_storage.list_checkpoints(workflow_name=workflow.name)
print(f"\nCheckpoints in Cosmos DB: {len(checkpoints)}")
for i, cp in enumerate(checkpoints[:5], 1):
print(f" {i}. {cp.checkpoint_id} (iteration={cp.iteration_count})")
# --- Second run: continue conversation with checkpoint history ---
print("\n=== Second Run (continuing conversation) ===\n")
query2 = "Can you elaborate on the economic benefits?"
print(f"User: {query2}")
response2 = await agent.run(query2, session=session, checkpoint_storage=checkpoint_storage)
for msg in response2.messages:
speaker = msg.author_name or msg.role
print(f"[{speaker}]: {msg.text}")
# Show total checkpoints
all_checkpoints = await checkpoint_storage.list_checkpoints(workflow_name=workflow.name)
print(f"\nTotal checkpoints after two runs: {len(all_checkpoints)}")
# Get latest checkpoint
latest = await checkpoint_storage.get_latest(workflow_name=workflow.name)
if latest:
print(f"Latest checkpoint: {latest.checkpoint_id}")
print(f" iteration_count: {latest.iteration_count}")
print(f" timestamp: {latest.timestamp}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,415 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import contextlib
import json
import sys
import uuid
from dataclasses import dataclass, field, replace
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any
from agent_framework import (
Executor,
FileCheckpointStorage,
SubWorkflowRequestMessage,
SubWorkflowResponseMessage,
Workflow,
WorkflowBuilder,
WorkflowContext,
WorkflowEvent,
WorkflowExecutor,
WorkflowRunState,
handler,
response_handler,
)
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
from typing_extensions import override # type: ignore[import] # pragma: no cover
CHECKPOINT_DIR = Path(__file__).with_suffix("").parent / "tmp" / "sub_workflow_checkpoints"
"""
Sample: Checkpointing for workflows that embed sub-workflows.
This sample shows how a parent workflow that wraps a sub-workflow can:
- run until the sub-workflow emits a human approval request
- persist a checkpoint that captures the pending request (including complex payloads)
- resume later, supplying the human decision directly at restore time
It is intentionally similar in spirit to the orchestration checkpoint sample but
uses ``WorkflowExecutor`` so we exercise the full parent/sub-workflow round-trip.
"""
def _utc_now() -> datetime:
return datetime.now()
# ---------------------------------------------------------------------------
# Messages exchanged inside the sub-workflow
# ---------------------------------------------------------------------------
@dataclass
class DraftTask:
"""Task handed from the parent to the sub-workflow writer."""
topic: str
due: datetime
iteration: int = 1
@dataclass
class DraftPackage:
"""Intermediate draft produced by the sub-workflow writer."""
topic: str
content: str
iteration: int
created_at: datetime = field(default_factory=_utc_now)
@dataclass
class FinalDraft:
"""Final deliverable returned to the parent workflow."""
topic: str
content: str
iterations: int
approved_at: datetime
@dataclass
class ReviewRequest:
"""Human approval request surfaced via `request_info`."""
id: str = str(uuid.uuid4())
topic: str = ""
iteration: int = 1
draft_excerpt: str = ""
due_iso: str = ""
reviewer_guidance: list[str] = field(default_factory=list) # type: ignore
@dataclass
class ReviewDecision:
"""The review decision to be sent to downstream executors along with the original request."""
decision: str
original_request: ReviewRequest
# ---------------------------------------------------------------------------
# Sub-workflow executors
# ---------------------------------------------------------------------------
class DraftWriter(Executor):
"""Produces an initial draft for the supplied topic."""
def __init__(self) -> None:
super().__init__(id="draft_writer")
@handler
async def create_draft(self, task: DraftTask, ctx: WorkflowContext[DraftPackage]) -> None:
draft = DraftPackage(
topic=task.topic,
content=(
f"Launch plan for {task.topic}.\n\n"
"- Outline the customer message.\n"
"- Highlight three differentiators.\n"
"- Close with a next-step CTA.\n"
f"(iteration {task.iteration})"
),
iteration=task.iteration,
)
await ctx.send_message(draft, target_id="draft_review")
class DraftReviewRouter(Executor):
"""Turns draft packages into human approval requests."""
def __init__(self) -> None:
super().__init__(id="draft_review")
@handler
async def request_review(self, draft: DraftPackage, ctx: WorkflowContext) -> None:
"""Request a review upon receiving a draft."""
excerpt = draft.content.splitlines()[0]
request = ReviewRequest(
topic=draft.topic,
iteration=draft.iteration,
draft_excerpt=excerpt,
due_iso=draft.created_at.isoformat(),
reviewer_guidance=[
"Ensure tone matches launch messaging",
"Confirm CTA is action-oriented",
],
)
await ctx.request_info(request_data=request, response_type=str)
@response_handler
async def forward_decision(
self,
original_request: ReviewRequest,
decision: str,
ctx: WorkflowContext[ReviewDecision],
) -> None:
"""Route the decision to the next executor."""
await ctx.send_message(ReviewDecision(decision=decision, original_request=original_request))
class DraftFinaliser(Executor):
"""Applies the human decision and emits the final draft."""
def __init__(self) -> None:
super().__init__(id="draft_finaliser")
@handler
async def on_review_decision(
self,
review_decision: ReviewDecision,
ctx: WorkflowContext[DraftTask, FinalDraft],
) -> None:
reply = review_decision.decision.strip().lower()
original = review_decision.original_request
topic = original.topic if original else "unknown topic"
iteration = original.iteration if original else 1
if reply != "approve":
# Loop back with a follow-up task. In a real workflow you would
# incorporate the human guidance; here we just increment the counter.
next_task = DraftTask(
topic=topic,
due=_utc_now() + timedelta(hours=1),
iteration=iteration + 1,
)
await ctx.send_message(next_task, target_id="draft_writer")
return
final = FinalDraft(
topic=topic,
content=f"Approved launch narrative for {topic} (iteration {iteration}).",
iterations=iteration,
approved_at=_utc_now(),
)
await ctx.yield_output(final)
# ---------------------------------------------------------------------------
# Parent workflow executors
# ---------------------------------------------------------------------------
class LaunchCoordinator(Executor):
"""Owns the top-level workflow and collects the final draft."""
def __init__(self) -> None:
super().__init__(id="launch_coordinator")
# Track pending requests to match responses
self._pending_requests: dict[str, SubWorkflowRequestMessage] = {}
@handler
async def kick_off(self, topic: str, ctx: WorkflowContext[DraftTask]) -> None:
task = DraftTask(topic=topic, due=_utc_now() + timedelta(hours=2))
await ctx.send_message(task)
@handler
async def collect_final(self, draft: FinalDraft, ctx: WorkflowContext[None, FinalDraft]) -> None:
approved_at = draft.approved_at
normalised = draft
if isinstance(approved_at, str):
with contextlib.suppress(ValueError):
parsed = datetime.fromisoformat(approved_at)
normalised = replace(draft, approved_at=parsed)
approved_at = parsed
approved_display = approved_at.isoformat() if hasattr(approved_at, "isoformat") else str(approved_at)
print("\n>>> Parent workflow received approved draft:")
print(f"- Topic: {normalised.topic}")
print(f"- Iterations: {normalised.iterations}")
print(f"- Approved at: {approved_display}")
print(f"- Content: {normalised.content}\n")
await ctx.yield_output(normalised)
@handler
async def handler_sub_workflow_request(
self,
request: SubWorkflowRequestMessage,
ctx: WorkflowContext,
) -> None:
"""Handle requests from the sub-workflow.
Note that the message type must be SubWorkflowRequestMessage to intercept the request.
"""
if not isinstance(request.source_event.data, ReviewRequest):
raise TypeError(f"Expected 'ReviewRequest', got {type(request.source_event.data)}")
# Record the request for response matching
review_request = request.source_event.data
self._pending_requests[review_request.id] = request
# Send the request without modification
await ctx.request_info(request_data=review_request, response_type=str)
@response_handler
async def handle_request_response(
self,
original_request: ReviewRequest,
response: str,
ctx: WorkflowContext[SubWorkflowResponseMessage],
) -> None:
"""Process the response and send it back to the sub-workflow.
Note that the response must be sent back using SubWorkflowResponseMessage to route
the response back to the sub-workflow.
"""
request_message = self._pending_requests.pop(original_request.id, None)
if request_message is None:
raise ValueError("No matching pending request found for the resource response")
await ctx.send_message(request_message.create_response(response))
@override
async def on_checkpoint_save(self) -> dict[str, Any]:
"""Capture any additional state needed for checkpointing."""
return {
"pending_requests": self._pending_requests,
}
@override
async def on_checkpoint_restore(self, state: dict[str, Any]) -> None:
"""Restore any additional state needed from checkpointing."""
self._pending_requests = state.get("pending_requests", {})
# ---------------------------------------------------------------------------
# Workflow construction helpers
# ---------------------------------------------------------------------------
def build_sub_workflow() -> WorkflowExecutor:
"""Assemble the sub-workflow used by the parent workflow executor."""
writer = DraftWriter()
router = DraftReviewRouter()
finaliser = DraftFinaliser()
sub_workflow = (
WorkflowBuilder(start_executor=writer)
.add_edge(writer, router)
.add_edge(router, finaliser)
.add_edge(finaliser, writer) # permits revision loops
.build()
)
return WorkflowExecutor(sub_workflow, id="launch_subworkflow")
def build_parent_workflow(storage: FileCheckpointStorage) -> Workflow:
"""Assemble the parent workflow that embeds the sub-workflow."""
coordinator = LaunchCoordinator()
sub_executor = build_sub_workflow()
return (
WorkflowBuilder(start_executor=coordinator, checkpoint_storage=storage)
.add_edge(coordinator, sub_executor)
.add_edge(sub_executor, coordinator)
.build()
)
async def main() -> None:
CHECKPOINT_DIR.mkdir(parents=True, exist_ok=True)
for file in CHECKPOINT_DIR.glob("*.json"):
file.unlink()
storage = FileCheckpointStorage(CHECKPOINT_DIR)
workflow = build_parent_workflow(storage)
print("\n=== Stage 1: run until sub-workflow requests human review ===")
request_id: str | None = None
async for event in workflow.run("Contoso Gadget Launch", stream=True):
if event.type == "request_info" and request_id is None:
request_id = event.request_id
print(f"Captured review request id: {request_id}")
if event.type == "status" and event.state is WorkflowRunState.IDLE_WITH_PENDING_REQUESTS:
break
if request_id is None:
raise RuntimeError("Sub-workflow completed without requesting review.")
resume_checkpoint = await storage.get_latest(workflow_name=workflow.name)
if not resume_checkpoint:
raise RuntimeError("No checkpoints found.")
# Print the checkpoint to show pending requests
# We didn't handle the request above so the request is still pending the last checkpoint
print(f"Using checkpoint {resume_checkpoint.checkpoint_id} at iteration {resume_checkpoint.iteration_count}")
checkpoint_path = storage.storage_path / f"{resume_checkpoint.checkpoint_id}.json"
if checkpoint_path.exists():
checkpoint_content_dict = json.loads(checkpoint_path.read_text())
print(f"Pending review requests: {checkpoint_content_dict.get('pending_request_info_events', {})}")
print("\n=== Stage 2: resume from checkpoint ===")
# Rebuild fresh instances to mimic a separate process resuming
workflow2 = build_parent_workflow(storage)
request_info_event: WorkflowEvent | None = None
async for event in workflow2.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True):
if event.type == "request_info":
request_info_event = event
if request_info_event is None:
raise RuntimeError("No request_info_event captured.")
print("\n=== Stage 3: approve draft ==")
approval_response = "approve"
output_event: WorkflowEvent | None = None
async for event in workflow2.run(stream=True, responses={request_info_event.request_id: approval_response}):
if event.type == "output":
output_event = event
if output_event is None:
raise RuntimeError("Workflow did not complete after resume.")
output = output_event.data
print("\n=== Final Draft (from resumed run) ===")
print(output)
""""
Sample Output:
=== Stage 1: run until sub-workflow requests human review ===
Captured review request id: 032c9f3a-ad1b-4a52-89be-a168d6663011
Using checkpoint 54f376c2-f849-44e4-9d8d-e627fd27ab96 at iteration 2
Pending review requests (sub executor snapshot): []
Pending review requests (parent executor snapshot): ['032c9f3a-ad1b-4a52-89be-a168d6663011']
=== Stage 2: resume from checkpoint and approve draft ===
>>> Parent workflow received approved draft:
- Topic: Contoso Gadget Launch
- Iterations: 1
- Approved at: 2025-09-25T14:29:34.479164
- Content: Approved launch narrative for Contoso Gadget Launch (iteration 1).
=== Final Draft (from resumed run) ===
FinalDraft(topic='Contoso Gadget Launch', content='Approved launch narrative for Contoso
Gadget Launch (iteration 1).', iterations=1, approved_at=datetime.datetime(2025, 9, 25, 14, 29, 34, 479164))
Coordinator stored final draft successfully.
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,186 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Sample: Workflow as Agent with Checkpointing
Purpose:
This sample demonstrates how to use checkpointing with a workflow wrapped as an agent.
It shows how to enable checkpoint storage when calling agent.run(),
allowing workflow execution state to be persisted and potentially resumed.
What you learn:
- How to pass checkpoint_storage to WorkflowAgent.run()
- How checkpoints are created during workflow-as-agent execution
- How to combine thread conversation history with workflow checkpointing
- How to resume a workflow-as-agent from a checkpoint
Key concepts:
- Thread (AgentSession): Maintains conversation history across agent invocations
- Checkpoint: Persists workflow execution state for pause/resume capability
- These are complementary: sessions track conversation, checkpoints track workflow state
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
"""
import asyncio
import os
from agent_framework import (
Agent,
InMemoryCheckpointStorage,
InMemoryHistoryProvider,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
async def basic_checkpointing() -> None:
"""Demonstrate basic checkpoint storage with workflow-as-agent."""
print("=" * 60)
print("Basic Checkpointing with Workflow as Agent")
print("=" * 60)
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
assistant = Agent(
client=client,
name="assistant",
instructions="You are a helpful assistant. Keep responses brief.",
)
reviewer = Agent(
client=client,
name="reviewer",
instructions="You are a reviewer. Provide a one-sentence summary of the assistant's response.",
)
workflow = SequentialBuilder(participants=[assistant, reviewer]).build()
agent = workflow.as_agent()
# Create checkpoint storage
checkpoint_storage = InMemoryCheckpointStorage()
# Run with checkpointing enabled
query = "What are the benefits of renewable energy?"
print(f"\nUser: {query}")
response = await agent.run(query, checkpoint_storage=checkpoint_storage)
for msg in response.messages:
speaker = msg.author_name or msg.role
print(f"[{speaker}]: {msg.text}")
# Show checkpoints that were created
checkpoints = await checkpoint_storage.list_checkpoints(workflow_name=workflow.name)
print(f"\nCheckpoints created: {len(checkpoints)}")
for i, cp in enumerate(checkpoints[:5], 1):
print(f" {i}. {cp.checkpoint_id}")
async def checkpointing_with_thread() -> None:
"""Demonstrate combining thread history with checkpointing."""
print("\n" + "=" * 60)
print("Checkpointing with Thread Conversation History")
print("=" * 60)
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
assistant = Agent(
client=client,
name="memory_assistant",
instructions="You are a helpful assistant with good memory. Reference previous conversation when relevant.",
)
workflow = SequentialBuilder(participants=[assistant]).build()
agent = workflow.as_agent()
# Create both session (for conversation) and checkpoint storage (for workflow state)
session = agent.create_session()
checkpoint_storage = InMemoryCheckpointStorage()
# First turn
query1 = "My favorite color is blue. Remember that."
print(f"\n[Turn 1] User: {query1}")
response1 = await agent.run(query1, session=session, checkpoint_storage=checkpoint_storage)
if response1.messages:
print(f"[assistant]: {response1.messages[0].text}")
# Second turn - agent should remember from session history
query2 = "What's my favorite color?"
print(f"\n[Turn 2] User: {query2}")
response2 = await agent.run(query2, session=session, checkpoint_storage=checkpoint_storage)
if response2.messages:
print(f"[assistant]: {response2.messages[0].text}")
# Show accumulated state
checkpoints = await checkpoint_storage.list_checkpoints(workflow_name=workflow.name)
print(f"\nTotal checkpoints across both turns: {len(checkpoints)}")
memory_state = session.state.get(InMemoryHistoryProvider.DEFAULT_SOURCE_ID, {})
history = memory_state.get("messages", [])
print(f"Messages in session history: {len(history)}")
async def streaming_with_checkpoints() -> None:
"""Demonstrate streaming with checkpoint storage."""
print("\n" + "=" * 60)
print("Streaming with Checkpointing")
print("=" * 60)
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
assistant = Agent(
client=client,
name="streaming_assistant",
instructions="You are a helpful assistant.",
)
workflow = SequentialBuilder(participants=[assistant]).build()
agent = workflow.as_agent()
checkpoint_storage = InMemoryCheckpointStorage()
query = "List three interesting facts about the ocean."
print(f"\nUser: {query}")
print("[assistant]: ", end="", flush=True)
# Stream with checkpointing
async for update in agent.run(query, checkpoint_storage=checkpoint_storage, stream=True):
if update.text:
print(update.text, end="", flush=True)
print() # Newline after streaming
checkpoints = await checkpoint_storage.list_checkpoints(workflow_name=workflow.name)
print(f"\nCheckpoints created during stream: {len(checkpoints)}")
async def main() -> None:
"""Run all checkpoint examples."""
await basic_checkpointing()
await checkpointing_with_thread()
await streaming_with_checkpoints()
if __name__ == "__main__":
asyncio.run(main())