diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py index dc90d13..88f409d 100644 --- a/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py @@ -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, @@ -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 @@ -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( diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_context.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_context.py index 4912fe4..66ecff1 100644 --- a/python/packages/azurefunctions/agent_framework_azurefunctions/_context.py +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_context.py @@ -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") diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_feature_usage.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_feature_usage.py new file mode 100644 index 0000000..4bc0ffe --- /dev/null +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_feature_usage.py @@ -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 diff --git a/python/packages/azurefunctions/pyproject.toml b/python/packages/azurefunctions/pyproject.toml index cd6c343..ff7bff8 100644 --- a/python/packages/azurefunctions/pyproject.toml +++ b/python/packages/azurefunctions/pyproject.toml @@ -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", diff --git a/python/packages/azurefunctions/tests/test_app.py b/python/packages/azurefunctions/tests/test_app.py index 15fff90..5751d3a 100644 --- a/python/packages/azurefunctions/tests/test_app.py +++ b/python/packages/azurefunctions/tests/test_app.py @@ -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]) @@ -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 diff --git a/python/packages/azurefunctions/tests/test_func_utils.py b/python/packages/azurefunctions/tests/test_func_utils.py index 80add1d..4d80feb 100644 --- a/python/packages/azurefunctions/tests/test_func_utils.py +++ b/python/packages/azurefunctions/tests/test_func_utils.py @@ -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.""" diff --git a/python/packages/durabletask/agent_framework_durabletask/_feature_usage.py b/python/packages/durabletask/agent_framework_durabletask/_feature_usage.py new file mode 100644 index 0000000..fba5fc3 --- /dev/null +++ b/python/packages/durabletask/agent_framework_durabletask/_feature_usage.py @@ -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 diff --git a/python/packages/durabletask/agent_framework_durabletask/_shim.py b/python/packages/durabletask/agent_framework_durabletask/_shim.py index e6e9f5d..ed8a752 100644 --- a/python/packages/durabletask/agent_framework_durabletask/_shim.py +++ b/python/packages/durabletask/agent_framework_durabletask/_shim.py @@ -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 @@ -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, diff --git a/python/packages/durabletask/agent_framework_durabletask/_worker.py b/python/packages/durabletask/agent_framework_durabletask/_worker.py index 3eed81f..77fae09 100644 --- a/python/packages/durabletask/agent_framework_durabletask/_worker.py +++ b/python/packages/durabletask/agent_framework_durabletask/_worker.py @@ -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 ( @@ -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: diff --git a/python/packages/durabletask/agent_framework_durabletask/_workflows/runner_context.py b/python/packages/durabletask/agent_framework_durabletask/_workflows/runner_context.py index d364616..b68de25 100644 --- a/python/packages/durabletask/agent_framework_durabletask/_workflows/runner_context.py +++ b/python/packages/durabletask/agent_framework_durabletask/_workflows/runner_context.py @@ -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") diff --git a/python/packages/durabletask/pyproject.toml b/python/packages/durabletask/pyproject.toml index f937c6b..6e5ca54 100644 --- a/python/packages/durabletask/pyproject.toml +++ b/python/packages/durabletask/pyproject.toml @@ -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", diff --git a/python/packages/durabletask/tests/test_runner_context.py b/python/packages/durabletask/tests/test_runner_context.py new file mode 100644 index 0000000..158cca7 --- /dev/null +++ b/python/packages/durabletask/tests/test_runner_context.py @@ -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) diff --git a/python/packages/durabletask/tests/test_shim.py b/python/packages/durabletask/tests/test_shim.py index de343f3..83dff6c 100644 --- a/python/packages/durabletask/tests/test_shim.py +++ b/python/packages/durabletask/tests/test_shim.py @@ -7,7 +7,7 @@ """ from typing import Any, cast -from unittest.mock import Mock +from unittest.mock import Mock, patch import pytest from agent_framework import Message, SupportsAgentRun @@ -15,6 +15,7 @@ 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 @@ -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 diff --git a/python/packages/durabletask/tests/test_worker.py b/python/packages/durabletask/tests/test_worker.py index 24f7ccb..fc96ffb 100644 --- a/python/packages/durabletask/tests/test_worker.py +++ b/python/packages/durabletask/tests/test_worker.py @@ -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 @@ -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( diff --git a/python/packages/durabletask/tests/test_workflow_activity.py b/python/packages/durabletask/tests/test_workflow_activity.py index de6650f..2b33bee 100644 --- a/python/packages/durabletask/tests/test_workflow_activity.py +++ b/python/packages/durabletask/tests/test_workflow_activity.py @@ -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) @@ -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) @@ -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) diff --git a/python/uv.lock b/python/uv.lock index f7fff65..19e5653 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -56,7 +56,7 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "agent-framework-core", specifier = ">=1.11.0,<2" }, + { name = "agent-framework-core", specifier = ">=1.13.0,<2" }, { name = "agent-framework-durabletask", editable = "packages/durabletask" }, { name = "azure-functions", specifier = ">=1.24.0,<2" }, { name = "azure-functions-durable", specifier = ">=1.3.1,<2" }, @@ -64,17 +64,18 @@ requires-dist = [ [[package]] name = "agent-framework-core" -version = "1.11.0" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "msgspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "python-dotenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/c2/e42a2ee46c1c30acc1082dd4881c032170f315f18355486094ee689925f1/agent_framework_core-1.11.0.tar.gz", hash = "sha256:758a06812b27cdba2a6c3d02ef6785971bd7b627a5b9482fb6defb3f0caf88ec", size = 498640, upload-time = "2026-07-10T03:42:25.475Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/96/14e04d00dfc2653677f3f97fa6b348bd202dc4bf4eba4621caabd93ee607/agent_framework_core-1.16.0.tar.gz", hash = "sha256:47ee37b4f6201add7a8a8f9cc39ffe43438c24360b53b116f61ba075e6962d94", size = 584318, upload-time = "2026-08-28T01:10:39.244Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/92/bad6983d03aee0323f56266d63d520347cd48e0b914b9b567fcd779bdb06/agent_framework_core-1.11.0-py3-none-any.whl", hash = "sha256:37e589e6086cf4a6ae6c7d2eaf4178f9fd1a81c13f863c81ad83df95258ce0a3", size = 551737, upload-time = "2026-07-10T03:35:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/ed/17/7abbfc9e560911967dd5b96a8b6cea537d35efab18f8f77367429159b637/agent_framework_core-1.16.0-py3-none-any.whl", hash = "sha256:382a6a0332cdc3144ffcf14b07a9a38c3ef42afc06dde8230226b850fe4cc76e", size = 641893, upload-time = "2026-08-28T01:10:29.875Z" }, ] [[package]] @@ -162,7 +163,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "agent-framework-core", specifier = ">=1.11.0,<2" }, + { name = "agent-framework-core", specifier = ">=1.13.0,<2" }, { name = "durabletask", specifier = ">=1.5.0,<2" }, { name = "durabletask-azuremanaged", specifier = ">=1.4.0,<2" }, { name = "python-dateutil", specifier = ">=2.8.0,<3" }, @@ -1920,6 +1921,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", size = 85384, upload-time = "2022-06-13T22:41:22.42Z" }, ] +[[package]] +name = "msgspec" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/60/f79b9b013a16fa3a58350c9295ddc6789f2e335f36ea61ed10a21b215364/msgspec-0.21.1.tar.gz", hash = "sha256:2313508e394b0d208f8f56892ca9b2799e2561329de9763b19619595a6c0f72c", size = 319193, upload-time = "2026-04-12T21:44:50.394Z" } + [[package]] name = "multidict" version = "6.7.1"