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,15 @@
# Copyright (c) Microsoft. All rights reserved.
import importlib.metadata
from ._app import AgentFunctionApp
try:
__version__ = importlib.metadata.version(__name__)
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0" # Fallback for development mode
__all__ = [
"AgentFunctionApp",
"__version__",
]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,181 @@
# Copyright (c) Microsoft. All rights reserved.
"""Runner context for Azure Functions activity execution.
This module provides the CapturingRunnerContext class that captures messages
and events produced during executor execution within Azure Functions activities.
"""
from __future__ import annotations
import asyncio
from copy import copy
from typing import Any
from agent_framework import (
CheckpointStorage,
RunnerContext,
WorkflowCheckpoint,
WorkflowEvent,
WorkflowMessage,
)
from agent_framework._workflows._runner_context import YieldOutputClassifier, YieldOutputEventType
from agent_framework._workflows._state import State
class CapturingRunnerContext(RunnerContext):
"""A RunnerContext implementation that captures messages and events for Azure Functions activities.
This context is designed for executing standard Executors within Azure Functions activities.
It captures all messages and events produced during execution without requiring durable
entity storage, allowing the results to be returned to the orchestrator.
Unlike InProcRunnerContext, this implementation does NOT support checkpointing
(always returns False for has_checkpointing). The orchestrator manages state
coordination; this context just captures execution output.
"""
def __init__(self) -> None:
"""Initialize the capturing runner context."""
self._messages: dict[str, list[WorkflowMessage]] = {}
self._event_queue: asyncio.Queue[WorkflowEvent] = asyncio.Queue()
self._pending_request_info_events: dict[str, WorkflowEvent[Any]] = {}
self._workflow_id: str | None = None
self._streaming: bool = False
self._yield_output_classifier: YieldOutputClassifier = lambda _executor_id: "output"
# region Messaging
async def send_message(self, message: WorkflowMessage) -> None:
"""Capture a message sent by an executor."""
self._messages.setdefault(message.source_id, [])
self._messages[message.source_id].append(message)
async def drain_messages(self) -> dict[str, list[WorkflowMessage]]:
"""Drain and return all captured messages."""
messages = copy(self._messages)
self._messages.clear()
return messages
async def has_messages(self) -> bool:
"""Check if there are any captured messages."""
return bool(self._messages)
# endregion Messaging
# region Events
async def add_event(self, event: WorkflowEvent) -> None:
"""Capture an event produced during execution."""
await self._event_queue.put(event)
async def drain_events(self) -> list[WorkflowEvent]:
"""Drain all currently queued events without blocking."""
events: list[WorkflowEvent] = []
while True:
try:
events.append(self._event_queue.get_nowait())
except asyncio.QueueEmpty:
break
return events
async def has_events(self) -> bool:
"""Check if there are any queued events."""
return not self._event_queue.empty()
async def next_event(self) -> WorkflowEvent:
"""Wait for and return the next event."""
return await self._event_queue.get()
# endregion Events
# region Checkpointing (not supported in activity context)
def has_checkpointing(self) -> bool:
"""Checkpointing is not supported in activity context."""
return False
def set_runtime_checkpoint_storage(self, storage: CheckpointStorage) -> None:
"""No-op: checkpointing not supported in activity context."""
pass
def clear_runtime_checkpoint_storage(self) -> None:
"""No-op: checkpointing not supported in activity context."""
pass
async def create_checkpoint(
self,
workflow_name: str,
graph_signature_hash: str,
state: State,
previous_checkpoint_id: str | None,
iteration_count: int,
metadata: dict[str, Any] | None = None,
) -> str:
"""Checkpointing not supported in activity context."""
raise NotImplementedError("Checkpointing is not supported in Azure Functions activity context")
async def load_checkpoint(self, checkpoint_id: str) -> WorkflowCheckpoint | None:
"""Checkpointing not supported in activity context."""
raise NotImplementedError("Checkpointing is not supported in Azure Functions activity context")
async def apply_checkpoint(self, checkpoint: WorkflowCheckpoint) -> None:
"""Checkpointing not supported in activity context."""
raise NotImplementedError("Checkpointing is not supported in Azure Functions activity context")
# endregion Checkpointing
# region Workflow Configuration
def set_workflow_id(self, workflow_id: str) -> None:
"""Set the workflow ID."""
self._workflow_id = workflow_id
def reset_for_new_run(self) -> None:
"""Reset the context for a new run."""
self._messages.clear()
self._event_queue = asyncio.Queue()
self._pending_request_info_events.clear()
self._streaming = False
def set_streaming(self, streaming: bool) -> None:
"""Set streaming mode (not used in activity context)."""
self._streaming = streaming
def is_streaming(self) -> bool:
"""Check if streaming mode is enabled (always False in activity context)."""
return self._streaming
def set_yield_output_classifier(self, classifier: YieldOutputClassifier) -> None:
"""Set the classifier used by WorkflowContext.yield_output()."""
self._yield_output_classifier = classifier
def classify_yielded_output(self, executor_id: str) -> YieldOutputEventType | None:
"""Classify an executor's yield_output payload as output, intermediate, or hidden."""
return self._yield_output_classifier(executor_id)
# endregion Workflow Configuration
# region Request Info Events
async def add_request_info_event(self, event: WorkflowEvent[Any]) -> None:
"""Add a request_info WorkflowEvent and track it for correlation."""
self._pending_request_info_events[event.request_id] = event
await self.add_event(event)
async def send_request_info_response(self, request_id: str, response: Any) -> None:
"""Send a response correlated to a pending request.
Note: This is not supported in activity context since human-in-the-loop
scenarios require orchestrator-level coordination.
"""
raise NotImplementedError(
"send_request_info_response is not supported in Azure Functions activity context. "
"Human-in-the-loop scenarios should be handled at the orchestrator level."
)
async def get_pending_request_info_events(self) -> dict[str, WorkflowEvent[Any]]:
"""Get the mapping of request IDs to their corresponding request_info events."""
return dict(self._pending_request_info_events)
# endregion Request Info Events
@@ -0,0 +1,118 @@
# Copyright (c) Microsoft. All rights reserved.
"""Durable Entity for Agent Execution.
This module defines a durable entity that manages agent state and execution.
Using entities instead of orchestrations provides better state management and
allows for long-running agent conversations.
"""
from __future__ import annotations
import logging
from collections.abc import Callable
from typing import Any, cast
import azure.durable_functions as df
from agent_framework import SupportsAgentRun
from agent_framework_durabletask import (
AgentEntity,
AgentEntityStateProviderMixin,
AgentResponseCallbackProtocol,
run_agent_coroutine,
)
logger = logging.getLogger("agent_framework.azurefunctions")
class AzureFunctionEntityStateProvider(AgentEntityStateProviderMixin):
"""Azure Functions Durable Entity state provider for AgentEntity.
This class utilizes the Durable Entity context from `azure-functions-durable` package
to get and set the state of the agent entity.
"""
def __init__(self, context: df.DurableEntityContext) -> None:
self._context = context
def _get_state_dict(self) -> dict[str, Any]:
raw_state = self._context.get_state(lambda: {})
if not isinstance(raw_state, dict):
return {}
return cast(dict[str, Any], raw_state)
def _set_state_dict(self, state: dict[str, Any]) -> None:
self._context.set_state(state)
def _get_thread_id_from_entity(self) -> str:
return str(self._context.entity_key)
def create_agent_entity(
agent: SupportsAgentRun,
callback: AgentResponseCallbackProtocol | None = None,
) -> Callable[[df.DurableEntityContext], None]:
"""Factory function to create an agent entity class.
Args:
agent: The Microsoft Agent Framework agent instance (must implement SupportsAgentRun)
callback: Optional callback invoked during streaming and final responses
Returns:
Entity function configured with the agent
"""
async def _entity_coroutine(context: df.DurableEntityContext) -> None:
"""Async handler that executes the entity operations."""
try:
logger.debug("[entity_function] Entity triggered")
logger.debug("[entity_function] Operation: %s", context.operation_name)
state_provider = AzureFunctionEntityStateProvider(context)
entity = AgentEntity(agent, callback, state_provider=state_provider)
operation = context.operation_name
if operation == "run" or operation == "run_agent":
input_data: Any = context.get_input()
request: str | dict[str, Any]
if isinstance(input_data, dict) and "message" in input_data:
request = cast(dict[str, Any], input_data)
else:
# Fall back to treating input as message string
request = "" if input_data is None else str(cast(object, input_data))
result = await entity.run(request)
context.set_result(result.to_dict())
elif operation == "reset":
entity.reset()
context.set_result({"status": "reset"})
else:
logger.error("[entity_function] Unknown operation: %s", operation)
context.set_result({"error": f"Unknown operation: {operation}"})
logger.info("[entity_function] Operation %s completed successfully", operation)
except Exception as exc:
logger.exception("[entity_function] Error executing entity operation %s", exc)
context.set_result({"error": str(exc), "status": "error"})
def entity_function(context: df.DurableEntityContext) -> None:
"""Synchronous wrapper invoked by the Durable Functions runtime.
All agent coroutines run on a single process-wide persistent event loop
(see ``run_agent_coroutine``). This keeps async resources created by
shared agent clients/credentials bound to a live loop across every
invocation, preventing cross-loop hangs when the host dispatches
successive entity operations onto different worker threads.
"""
try:
run_agent_coroutine(_entity_coroutine(context))
except Exception as exc: # pragma: no cover - defensive logging
logger.error("[entity_function] Unexpected error executing entity: %s", exc, exc_info=True)
context.set_result({"error": str(exc), "status": "error"})
return entity_function
@@ -0,0 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
"""Custom exception types for the durable agent framework."""
class IncomingRequestError(ValueError):
"""Raised when an incoming HTTP request cannot be parsed or validated."""
def __init__(self, message: str, status_code: int = 400) -> None:
super().__init__(message)
self.status_code = status_code
@@ -0,0 +1,222 @@
# Copyright (c) Microsoft. All rights reserved.
"""Orchestration Support for Durable Agents.
This module provides support for using agents inside Durable Function orchestrations.
"""
import logging
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, TypeAlias
import azure.durable_functions as df
from agent_framework import AgentSession
from agent_framework_durabletask import (
DurableAgentExecutor,
RunRequest,
ensure_response_format,
load_agent_response,
)
from azure.durable_functions.models import TaskBase
from azure.durable_functions.models.actions.NoOpAction import NoOpAction
from azure.durable_functions.models.Task import CompoundTask, TaskState
from pydantic import BaseModel
logger = logging.getLogger("agent_framework.azurefunctions")
CompoundActionConstructor: TypeAlias = Callable[[list[Any]], Any] | None
if TYPE_CHECKING:
from azure.durable_functions import DurableOrchestrationContext
class _TypedCompoundTask(CompoundTask):
_first_error: Any
def __init__(
self,
tasks: list[TaskBase],
compound_action_constructor: CompoundActionConstructor = None,
) -> None: ...
AgentOrchestrationContextType: TypeAlias = DurableOrchestrationContext
else:
AgentOrchestrationContextType = Any
_TypedCompoundTask = CompoundTask
class PreCompletedTask(TaskBase):
"""A simple task that is already completed with a result.
Used for fire-and-forget mode where we want to return immediately
with an acceptance response without waiting for entity processing.
"""
def __init__(self, result: Any):
"""Initialize with a completed result.
Args:
result: The result value for this completed task
"""
# Initialize with a NoOp action since we don't need actual orchestration actions
super().__init__(-1, NoOpAction())
# Immediately mark as completed with the result
self.set_value(is_error=False, value=result)
class AgentTask(_TypedCompoundTask):
"""A custom Task that wraps entity calls and provides typed AgentResponse results.
This task wraps the underlying entity call task and intercepts its completion
to convert the raw result into a typed AgentResponse object.
"""
def __init__(
self,
entity_task: TaskBase,
response_format: type[BaseModel] | None,
correlation_id: str,
):
"""Initialize the AgentTask.
Args:
entity_task: The underlying entity call task
response_format: Optional Pydantic model for response parsing
correlation_id: Correlation ID for logging
"""
# Set instance variables BEFORE calling super().__init__
# because super().__init__ may trigger try_set_value for pre-completed tasks
self._response_format = response_format
self._correlation_id = correlation_id
super().__init__([entity_task])
# Override action_repr to expose the inner task's action directly
# This ensures compatibility with ReplaySchema V3 which expects Action objects.
self.action_repr = entity_task.action_repr
# Also copy the task ID to match the entity task's identity
self.id = entity_task.id
def try_set_value(self, child: TaskBase) -> None:
"""Transition the AgentTask to a terminal state and set its value to `AgentResponse`.
Parameters
----------
child : TaskBase
The entity call task that just completed
"""
if child.state is TaskState.SUCCEEDED:
# Delegate to parent class for standard completion logic
if len(self.pending_tasks) == 0:
# Transform the raw result before setting it
raw_result = child.result
logger.debug(
"[AgentTask] Converting raw result for correlation_id %s",
self._correlation_id,
)
try:
response = load_agent_response(raw_result)
if self._response_format is not None:
ensure_response_format(
self._response_format,
self._correlation_id,
response,
)
# Set the typed AgentResponse as this task's result
self.set_value(is_error=False, value=response)
except Exception as e:
logger.exception(
"[AgentTask] Failed to convert result for correlation_id: %s",
self._correlation_id,
)
self.set_value(is_error=True, value=e)
else:
# If error not handled by the parent, set it explicitly.
if self._first_error is None:
self._first_error = child.result
self.set_value(is_error=True, value=self._first_error)
class AzureFunctionsAgentExecutor(DurableAgentExecutor[AgentTask]):
"""Executor that executes durable agents inside Azure Functions orchestrations."""
def __init__(self, context: AgentOrchestrationContextType):
self.context = context
def generate_unique_id(self) -> str:
return str(self.context.new_uuid())
def get_run_request(
self,
message: str,
*,
options: dict[str, Any] | None = None,
) -> RunRequest:
"""Get the current run request from the orchestration context.
Args:
message: The message to send to the agent
options: Optional options dictionary. Supported keys include
``response_format``, ``enable_tool_calls``, and ``wait_for_response``.
Additional keys are forwarded to the agent execution.
Returns:
RunRequest: The current run request
Raises:
ValueError: If wait_for_response=False (not supported in orchestrations)
"""
# Create a copy to avoid modifying the caller's dict
request = super().get_run_request(message, options=options)
request.orchestration_id = self.context.instance_id
return request
def run_durable_agent(
self,
agent_name: str,
run_request: RunRequest,
session: AgentSession | None = None,
) -> AgentTask:
# Resolve session
session_id = self._create_session_id(agent_name, session)
entity_id = df.EntityId(
name=session_id.entity_name,
key=session_id.key,
)
logger.debug(
"[AzureFunctionsAgentProvider] correlation_id: %s entity_id: %s session_id: %s",
run_request.correlation_id,
entity_id,
session_id,
)
# Branch based on wait_for_response
if not run_request.wait_for_response:
# Fire-and-forget mode: signal entity and return pre-completed task
logger.debug(
"[AzureFunctionsAgentExecutor] Fire-and-forget mode: signaling entity (correlation: %s)",
run_request.correlation_id,
)
self.context.signal_entity(entity_id, "run", run_request.to_dict())
# Create acceptance response using base class helper
acceptance_response = self._create_acceptance_response(run_request.correlation_id)
# Create a pre-completed task with the acceptance response
entity_task = PreCompletedTask(acceptance_response)
else:
# Blocking mode: call entity and wait for response
entity_task = self.context.call_entity(entity_id, "run", run_request.to_dict())
return AgentTask(
entity_task=entity_task,
response_format=run_request.response_format,
correlation_id=run_request.correlation_id,
)
@@ -0,0 +1,83 @@
# Copyright (c) Microsoft. All rights reserved.
"""Workflow Execution for Durable Functions.
This module provides the Azure Functions entry point for workflow orchestration.
The actual orchestration logic lives in the shared module
``agent_framework_durabletask._workflows.orchestrator`` and is host-agnostic.
This module re-exports the public API and provides the AF-specific
``run_workflow_orchestrator`` wrapper that creates an
:class:`AzureFunctionsWorkflowContext` before delegating.
"""
from __future__ import annotations
import logging
from collections.abc import Generator
from typing import Any
from agent_framework import Workflow
from agent_framework_durabletask._workflows.orchestrator import (
SOURCE_HITL_RESPONSE,
SOURCE_ORCHESTRATOR,
SOURCE_WORKFLOW_START,
ExecutorResult,
PendingHITLRequest,
TaskMetadata,
TaskType,
_extract_message_content, # pyright: ignore[reportPrivateUsage]
build_agent_executor_response,
execute_hitl_response_handler,
route_message_through_edge_groups,
)
from agent_framework_durabletask._workflows.orchestrator import (
run_workflow_orchestrator as _run_workflow_orchestrator_shared,
)
from azure.durable_functions import DurableOrchestrationContext
from ._workflow_af_context import AzureFunctionsWorkflowContext
logger = logging.getLogger(__name__)
# Re-export shared symbols for backward compatibility
__all__ = [
"SOURCE_HITL_RESPONSE",
"SOURCE_ORCHESTRATOR",
"SOURCE_WORKFLOW_START",
"ExecutorResult",
"PendingHITLRequest",
"TaskMetadata",
"TaskType",
"_extract_message_content",
"build_agent_executor_response",
"execute_hitl_response_handler",
"route_message_through_edge_groups",
"run_workflow_orchestrator",
]
def run_workflow_orchestrator(
context: DurableOrchestrationContext,
workflow: Workflow,
initial_message: Any,
shared_state: dict[str, Any] | None = None,
) -> Generator[Any, Any, list[Any] | dict[str, Any]]:
"""Azure Functions wrapper around the shared workflow orchestrator.
Creates an :class:`AzureFunctionsWorkflowContext` and delegates to the
host-agnostic :func:`run_workflow_orchestrator` in the durabletask package.
Args:
context: The Azure Functions ``DurableOrchestrationContext``.
workflow: The MAF Workflow instance to execute.
initial_message: Initial message to send to the start executor.
shared_state: Optional dict for cross-executor state sharing.
Returns:
For a top-level run, the list of workflow outputs collected from executor
activities. For a sub-workflow run, a result envelope ``{"outputs": [...],
"events": [...]}`` so the parent can bubble nested progress (see the shared
``run_workflow_orchestrator`` in the durabletask package).
"""
af_ctx = AzureFunctionsWorkflowContext(context)
return _run_workflow_orchestrator_shared(af_ctx, workflow, initial_message, shared_state)
@@ -0,0 +1,105 @@
# Copyright (c) Microsoft. All rights reserved.
"""Azure Functions adapter for WorkflowOrchestrationContext.
Wraps ``azure.durable_functions.DurableOrchestrationContext`` to satisfy the
:class:`~agent_framework_durabletask.WorkflowOrchestrationContext` protocol.
"""
from __future__ import annotations
import logging
from datetime import datetime
from typing import Any
from agent_framework_durabletask import AgentSessionId, DurableAgentSession, DurableAIAgent
from azure.durable_functions import DurableOrchestrationContext
from ._orchestration import AzureFunctionsAgentExecutor
logger = logging.getLogger(__name__)
class AzureFunctionsWorkflowContext:
"""Adapter that maps ``DurableOrchestrationContext`` to ``WorkflowOrchestrationContext``."""
def __init__(self, context: DurableOrchestrationContext) -> None:
self._context = context
# -- Properties -----------------------------------------------------------
@property
def instance_id(self) -> str:
# Typed local (not cast): mypy sees the untyped context as Any, while
# pyright sees a concrete str - the annotation satisfies both.
instance_id: str = self._context.instance_id
return instance_id
@property
def is_replaying(self) -> bool:
is_replaying: bool = self._context.is_replaying
return is_replaying
@property
def supports_event_streaming(self) -> bool:
# The Azure Functions host has no workflow event-streaming endpoint, and its
# Durable Functions custom status is capped at 16 KB by the WebJobs extension.
# Publishing the accumulating event log would overflow that cap and fail the
# orchestrator, so events are omitted; state, pending HITL requests, and the
# final output remain available via the workflow status endpoint.
return False
@property
def current_utc_datetime(self) -> datetime:
current: datetime = self._context.current_utc_datetime
return current
# -- Agent / Activity dispatch --------------------------------------------
def prepare_agent_task(self, executor_id: str, message: str, orchestration_instance_id: str) -> Any:
session_id = AgentSessionId(name=executor_id, key=orchestration_instance_id)
session = DurableAgentSession(durable_session_id=session_id)
az_executor = AzureFunctionsAgentExecutor(self._context)
agent = DurableAIAgent(az_executor, executor_id)
return agent.run(message, session=session)
def prepare_activity_task(self, activity_name: str, input_json: str) -> Any:
orchestration_context: Any = self._context
return orchestration_context.call_activity(activity_name, input_json)
def call_sub_orchestrator(self, name: str, input: Any, instance_id: str | None = None) -> Any:
orchestration_context: Any = self._context
return orchestration_context.call_sub_orchestrator(name, input_=input, instance_id=instance_id)
# -- Composite tasks ------------------------------------------------------
def task_all(self, tasks: list[Any]) -> Any:
return self._context.task_all(tasks)
def task_any(self, tasks: list[Any]) -> Any:
return self._context.task_any(tasks)
# -- External events / timers ---------------------------------------------
def wait_for_external_event(self, name: str) -> Any:
return self._context.wait_for_external_event(name)
def create_timer(self, fire_at: datetime) -> Any:
return self._context.create_timer(fire_at)
# -- Status / utility -----------------------------------------------------
def set_custom_status(self, status: Any) -> None:
self._context.set_custom_status(status)
def new_uuid(self) -> str:
new_uuid: str = self._context.new_uuid()
return new_uuid
def cancel_task(self, task: Any) -> None:
cancel_fn = getattr(task, "cancel", None)
if callable(cancel_fn):
cancel_fn()
def get_task_result(self, task: Any) -> Any:
return getattr(task, "result", None)