Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import azure.durable_functions as df
import azure.functions as func
from agent_framework import SupportsAgentRun, Workflow
from agent_framework._telemetry import mark_feature_used
from agent_framework_durabletask import (
DEFAULT_MAX_POLL_RETRIES,
DEFAULT_POLL_INTERVAL_SECONDS,
Expand Down Expand Up @@ -58,6 +59,7 @@

from ._entities import create_agent_entity
from ._errors import IncomingRequestError
from ._feature_usage import FeatureIndex
from ._orchestration import AgentOrchestrationContextType, AgentTask, AzureFunctionsAgentExecutor
from ._routes import build_workflow_respond_url, build_workflow_status_url, split_request_url
from ._workflow import run_workflow_orchestrator
Expand Down Expand Up @@ -315,6 +317,7 @@ def __init__(
if self.enable_health_check:
self._setup_health_route()

mark_feature_used(FeatureIndex.AZUREFUNCTIONS)
logger.debug("[AgentFunctionApp] Initialization complete")

def _collect_workflows(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ async def create_checkpoint(
"""Checkpointing not supported in activity context."""
raise NotImplementedError("Checkpointing is not supported in Azure Functions activity context")

async def build_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,
) -> WorkflowCheckpoint:
"""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")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.

from enum import IntEnum


class FeatureIndex(IntEnum):
"""Azure Functions-owned feature-usage indexes."""

AZUREFUNCTIONS = 78
2 changes: 1 addition & 1 deletion python/packages/azurefunctions/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.11.0,<2",
"agent-framework-core>=1.13.0,<2",
"agent-framework-durabletask>=1.0.0b260709,<2",
"azure-functions>=1.24.0,<2",
"azure-functions-durable>=1.3.1,<2",
Expand Down
6 changes: 5 additions & 1 deletion python/packages/azurefunctions/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
)
from agent_framework_azurefunctions._entities import create_agent_entity
from agent_framework_azurefunctions._errors import IncomingRequestError
from agent_framework_azurefunctions._feature_usage import FeatureIndex

FuncT = TypeVar("FuncT", bound=Callable[..., Any])

Expand Down Expand Up @@ -63,8 +64,11 @@ def test_init_with_defaults(self) -> None:
mock_agent = Mock()
mock_agent.name = "TestAgent"

app = AgentFunctionApp(agents=[mock_agent])
with patch("agent_framework_azurefunctions._app.mark_feature_used") as mark_feature_used:
app = AgentFunctionApp(agents=[mock_agent])

mark_feature_used.assert_called_once_with(FeatureIndex.AZUREFUNCTIONS)
assert FeatureIndex.AZUREFUNCTIONS == 78
assert len(app.agents) == 1
assert "TestAgent" in app.agents
assert app.enable_health_check is True
Expand Down
8 changes: 8 additions & 0 deletions python/packages/azurefunctions/tests/test_func_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ async def test_create_checkpoint_raises_not_implemented(self, context: Capturing
with pytest.raises(NotImplementedError):
await context.create_checkpoint("test_workflow", "abc123", State(), None, 1)

@pytest.mark.asyncio
async def test_build_checkpoint_raises_not_implemented(self, context: CapturingRunnerContext) -> None:
"""Test that checkpoint construction is not supported."""
from agent_framework._workflows._state import State

with pytest.raises(NotImplementedError):
await context.build_checkpoint("test_workflow", "abc123", State(), None, 1)

@pytest.mark.asyncio
async def test_load_checkpoint_raises_not_implemented(self, context: CapturingRunnerContext) -> None:
"""Test that load_checkpoint raises NotImplementedError."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.

from enum import IntEnum


class FeatureIndex(IntEnum):
"""Durable Task-owned feature-usage indexes."""

DURABLETASK = 77
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
from typing import Any, Generic, Literal, TypeVar

from agent_framework import AgentSession, ServiceSessionId, SupportsAgentRun, normalize_messages
from agent_framework._telemetry import mark_feature_used
from agent_framework._types import AgentRunInputs

from ._executors import DurableAgentExecutor
from ._feature_usage import FeatureIndex
from ._models import DurableAgentSession

# TypeVar for the task type returned by executors
Expand Down Expand Up @@ -127,6 +129,7 @@ def run( # type: ignore[override]
options=options,
)

mark_feature_used(FeatureIndex.DURABLETASK)
return self._executor.run_durable_agent(
agent_name=self.name,
run_request=run_request,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
from typing import Any

from agent_framework import SupportsAgentRun, Workflow
from agent_framework._telemetry import mark_feature_used
from durabletask.task import ActivityContext, OrchestrationContext
from durabletask.worker import TaskHubGrpcWorker

from ._async_bridge import run_agent_coroutine
from ._callbacks import AgentResponseCallbackProtocol
from ._entities import AgentEntity, DurableTaskEntityStateProvider
from ._feature_usage import FeatureIndex
from ._workflows.activity import execute_workflow_activity
from ._workflows.dt_context import DurableTaskWorkflowContext
from ._workflows.naming import (
Expand Down Expand Up @@ -157,6 +159,7 @@ def start(self) -> None:
The worker will block until stopped.
"""
logger.info("[DurableAIAgentWorker] Starting worker with %d registered agents", len(self._registered_agents))
mark_feature_used(FeatureIndex.DURABLETASK)
self._worker.start()

def stop(self) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ async def create_checkpoint(
) -> str:
raise NotImplementedError("Checkpointing is not supported in activity context")

async def build_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,
) -> WorkflowCheckpoint:
raise NotImplementedError("Checkpointing is not supported in activity context")

async def load_checkpoint(self, checkpoint_id: str) -> WorkflowCheckpoint | None:
raise NotImplementedError("Checkpointing is not supported in activity context")

Expand Down
2 changes: 1 addition & 1 deletion python/packages/durabletask/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.11.0,<2",
"agent-framework-core>=1.13.0,<2",
"durabletask>=1.5.0,<2",
"durabletask-azuremanaged>=1.4.0,<2",
"python-dateutil>=2.8.0,<3",
Expand Down
15 changes: 15 additions & 0 deletions python/packages/durabletask/tests/test_runner_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright (c) Microsoft. All rights reserved.

import pytest
from agent_framework._workflows._state import State

from agent_framework_durabletask._workflows.runner_context import CapturingRunnerContext


@pytest.mark.asyncio
async def test_build_checkpoint_raises_not_implemented() -> None:
"""Checkpoint construction must not fall through to the protocol stub."""
context = CapturingRunnerContext()

with pytest.raises(NotImplementedError):
await context.build_checkpoint("test_workflow", "abc123", State(), None, 1)
7 changes: 5 additions & 2 deletions python/packages/durabletask/tests/test_shim.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
"""

from typing import Any, cast
from unittest.mock import Mock
from unittest.mock import Mock, patch

import pytest
from agent_framework import Message, SupportsAgentRun
from pydantic import BaseModel

from agent_framework_durabletask import DurableAgentSession
from agent_framework_durabletask._executors import DurableAgentExecutor
from agent_framework_durabletask._feature_usage import FeatureIndex
from agent_framework_durabletask._models import RunRequest
from agent_framework_durabletask._shim import DurableAgentProvider, DurableAIAgent

Expand Down Expand Up @@ -67,8 +68,10 @@ class TestDurableAIAgentMessageNormalization:

def test_run_accepts_string_message(self, test_agent: DurableAIAgent[Any], mock_executor: Mock) -> None:
"""Verify run accepts and normalizes string messages."""
test_agent.run("Hello, world!")
with patch("agent_framework_durabletask._shim.mark_feature_used") as mark_feature_used:
test_agent.run("Hello, world!")

mark_feature_used.assert_called_once_with(FeatureIndex.DURABLETASK)
mock_executor.run_durable_agent.assert_called_once()
# Verify agent_name and run_request were passed correctly as kwargs
_, kwargs = mock_executor.run_durable_agent.call_args
Expand Down
8 changes: 6 additions & 2 deletions python/packages/durabletask/tests/test_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
Focuses on critical worker flows: agent registration, validation, callbacks, and lifecycle.
"""

from unittest.mock import Mock
from unittest.mock import Mock, patch

import pytest

from agent_framework_durabletask import DurableAIAgentWorker
from agent_framework_durabletask._feature_usage import FeatureIndex


@pytest.fixture
Expand Down Expand Up @@ -131,8 +132,11 @@ def test_start_delegates_to_underlying_worker(
self, agent_worker: DurableAIAgentWorker, mock_grpc_worker: Mock
) -> None:
"""Verify start() delegates to wrapped worker."""
agent_worker.start()
with patch("agent_framework_durabletask._worker.mark_feature_used") as mark_feature_used:
agent_worker.start()

mark_feature_used.assert_called_once_with(FeatureIndex.DURABLETASK)
assert FeatureIndex.DURABLETASK == 77
mock_grpc_worker.start.assert_called_once()

def test_stop_delegates_to_underlying_worker(
Expand Down
9 changes: 7 additions & 2 deletions python/packages/durabletask/tests/test_workflow_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ async def mutate(message: Any, source_executor_ids: Any, state: Any, runner_cont
config = state.get("Local.config")
config["code"] = "SOMECODEXXX"
config["enabled"] = True
state.set("Local.config", config)
state.commit()

executor = _make_executor("test-exec", mutate)
Expand All @@ -68,7 +69,9 @@ def test_new_key_in_nested_dict_detected(self) -> None:
"""Adding a key to a nested dict is reported as an update."""

async def mutate(message: Any, source_executor_ids: Any, state: Any, runner_context: Any) -> None:
state.get("Local.data")["code"] = "NEW_CODE"
data = state.get("Local.data")
data["code"] = "NEW_CODE"
state.set("Local.data", data)
state.commit()

executor = _make_executor("test-exec", mutate)
Expand All @@ -80,7 +83,9 @@ def test_nested_list_mutation_detected(self) -> None:
"""Appending to a nested list is reported as an update."""

async def mutate(message: Any, source_executor_ids: Any, state: Any, runner_context: Any) -> None:
state.get("Local.items").append(4)
items = state.get("Local.items")
items.append(4)
state.set("Local.items", items)
state.commit()

executor = _make_executor("test-exec", mutate)
Expand Down
17 changes: 12 additions & 5 deletions python/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading