feat(workflows): support async workflow tool handlers - #233
Tsuyoshi Ushio (TsuyoshiUshio) merged 7 commits into
Conversation
The workflow tool Activity already awaits coroutine handlers, but the registry still rejected async def handlers from the v1 design. Remove that rejection, add tests for async success, failure, and non-JSON results on the legacy and policy-aware paths, and update the docs. Refs #139 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Make fetch_deploys an async def handler that awaits a simulated deploy-history API call, and test that it runs through the workflow tool Activity. Refs #139 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The schema-wrapper invocation issue and documented guidance inconsistencies remain, and host-level end-to-end coverage is still needed.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
What changed in this PR
This PR adds support for async workflow tool handlers while preserving synchronous execution and retry behavior.
Changes:
- Accepts and awaits async handlers.
- Adds registration, discovery, Activity, retry, error, and result-validation tests.
- Updates workflow documentation, FRD guidance, and sample comments.
| File | Summary | Final review notes |
|---|---|---|
tests/test_workflow_registry.py |
Tests async registration. | — |
tests/test_workflow_native_retry.py |
Tests Activity execution and retries. | Nit (1 vote): Add Functions-host/Durable end-to-end coverage. |
tests/test_workflow_activity.py |
Tests async failures and result contracts. | — |
tests/test_discovery_tools.py |
Tests async tool discovery. | — |
src/azure_functions_agents/workflows/registry.py |
Removes async-handler rejection. | Moderate (1 vote): Schema-wrapped dual-decorated tools can fail with TypeError; adapt the workflow adapter and add an invocation test. Nit (1 vote): Update the stale async-guard comment. |
samples/workflow-incident-triage/src/tools/incident_tools.py |
Updates sample guidance. | — |
docs/workflows.md |
Documents async handlers and usage. | Nits (2 votes, 1 vote): Update contradictory front-matter guidance and clarify timeout cancellation differences. |
docs/frds/0004-dynamic-workflows.md |
Updates handler requirements. | Nit (1 vote): Revise contradictory dual-decorator guidance. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Convert workflow argument dictionaries to schema models for both decorator orders. Keep synchronous handlers on worker threads and await async handlers. Update the authoring guidance. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Fixed the schema-wrapper invocation issue in 5d9f0fe. Workflow discovery now selects a dictionary-to-model adapter for tools that use both decorators. Both decorator orders work with synchronous and async handlers. Synchronous handlers stay on a worker thread, and the normal MAF wrapper is unchanged. Added regression coverage for invocation, model validation, decorator order, and thread selection. Updated the related authoring and timeout guidance. Functions-host/Durable end-to-end coverage was not added in this update. |
Laveesh Rohra (larohra)
left a comment
There was a problem hiding this comment.
Could you add a Functions-host/Durable end-to-end test that exercises an async @workflow_tool through the deployed workflow path?
The current tests invoke the generated Activity directly and validate the registry/catalog behavior, but they do not verify that the Functions host can discover the handler, register the workflow, schedule the Durable Activity, await the async result, serialize it, and complete the workflow successfully. This PR changes the registry → catalog → Activity boundary, so a host-level test is important to catch integration issues that unit tests cannot expose.
This is also part of issue #139's acceptance criteria. Please add the test under the existing workflow E2E coverage, or link a follow-up that explicitly tracks it if it cannot be included in this PR.
Run the workflow-incident-triage sample under func start and complete a workflow in which the async fetch_deploys tool and synchronous tools feed summarize_findings through Durable. Refs #139 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Laveesh Rohra (@larohra) Added a Functions-host/Durable E2E test in 903be15: |
Keep polling when the new instance is briefly not visible to the status API, and use a dedicated Durable task hub so other E2E hosts cannot affect the run. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace the model-free Durable check with an agent-driven E2E. The test sends a chat prompt to the workflow-incident-triage sample through Foundry, lets the agent author the plan and call start_workflow, and confirms that the async fetch_deploys handler completes in the Durable Activity. Status polling tolerates one slow or dropped request and includes host output on failure. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Laveesh Rohra (@larohra) Follow-up: I changed the E2E test in dc0acc0. It now runs end to end through a live agent. It does not call the Durable HTTP API directly.
Status polling continues after one slow or dropped request until the overall deadline. If the test fails, the message includes the host output. The test uses a dedicated task hub ( |
Rename the E2E module to test_workflow_tools_e2e.py, because the workflow runs both sync and async handlers. The test now also checks the results of the sync fetch_logs and fetch_metrics tools. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Closes #139 and https://github.com/Azure/azure-functions-bucees-planning/issues/1303
Summary
@workflow_toolnow acceptsasync defhandlers. Workflow tools can call async SDKs and other I/O-bound APIs without a synchronous wrapper.Why this change is small
The first Dynamic Workflows version ran the tool Activity as a synchronous function and called
handler(args)directly. An async handler returned a coroutine that Durable could not serialize, so the registry rejectedasync defhandlers.The retry work (#193) changed the tool Activity to
async defand addedinvoke_handler():But the registry still rejected async handlers. This PR removes that old check.
The orchestrator does not change. It still uses
def+yield, which the Python Durable SDK requires. Async code runs only inside the Activity.Changes
workflows/registry.py: remove theinspect.iscoroutinefunctionrejection and update the docstrings.@workflow_toolin the handler catalog.WorkflowRetryableErrorstarts a Durable retry. An unexpected error message is hidden. A result that is not JSON is a contract failure. (Async timeout already had a test.)docs/workflows.md(async example), FRD 0004 handler contract, and a comment in the incident-triage sample.Validation
ruff check src tests: passmypy src: passpytest tests: 1281 passedNot included
This PR does not add an end-to-end test that runs on the Functions host (#139 asks for one). I can add it in a follow-up PR.