Python: (core): Add functional workflow API#4238
Python: (core): Add functional workflow API#4238eavanvalkenburg merged 17 commits intomicrosoft:mainfrom
Conversation
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Pull request overview
This PR introduces a functional workflow API as an alternative to the existing graph-based workflow API. The functional approach allows users to write workflows as plain async functions decorated with @workflow, using native Python control flow (if/else, loops, asyncio.gather) instead of explicit graph construction with executors and edges. The @step decorator is optional and provides per-step checkpointing, caching, and observability.
Changes:
- Added core implementation (
_functional.py) with@workflow,@stepdecorators,RunContext,FunctionalWorkflow, andFunctionalWorkflowAgentclasses - Added comprehensive test suite (40+ test cases covering basic execution, HITL, checkpointing, streaming, error handling, edge cases)
- Added 6 sample files demonstrating functional workflows (basic pipeline, streaming, parallel execution, checkpointing, HITL, agent integration)
- Restructured getting-started samples to introduce functional workflows before graph workflows
- Updated exports in
__init__.pyto expose new functional API symbols
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
python/packages/core/agent_framework/_workflows/_functional.py |
Core implementation of functional workflow API with RunContext, StepWrapper, FunctionalWorkflow, and FunctionalWorkflowAgent classes (1105 lines) |
python/packages/core/agent_framework/__init__.py |
Added exports for FunctionalWorkflow, FunctionalWorkflowAgent, RunContext, StepWrapper, step, and workflow |
python/packages/core/tests/workflow/test_functional_workflow.py |
Comprehensive test suite covering basic execution, events, parallelism, HITL, errors, streaming, state, checkpointing, control flow, and edge cases (1031 lines) |
python/samples/01-get-started/05_first_functional_workflow.py |
Getting started sample demonstrating basic functional workflow with plain async functions |
python/samples/01-get-started/06_first_graph_workflow.py |
Renamed and updated graph workflow sample (previously 05_first_workflow.py) |
python/samples/01-get-started/07_host_your_agent.py |
Renamed agent hosting sample (previously 06_host_your_agent.py) |
python/samples/01-get-started/README.md |
Updated sample listing to include both functional and graph workflow samples |
python/samples/03-workflows/functional/basic_pipeline.py |
Sample showing simplest sequential pipeline with @workflow decorator |
python/samples/03-workflows/functional/basic_streaming_pipeline.py |
Sample demonstrating streaming workflow events with run(stream=True) |
python/samples/03-workflows/functional/parallel_pipeline.py |
Sample showing fan-out/fan-in with asyncio.gather |
python/samples/03-workflows/functional/steps_and_checkpointing.py |
Sample explaining @step decorator for per-step checkpointing and observability |
python/samples/03-workflows/functional/hitl_review.py |
Sample demonstrating HITL with ctx.request_info() and resume |
python/samples/03-workflows/functional/agent_integration.py |
Sample showing agent calls inside workflows and .as_agent() wrapper |
python/samples/03-workflows/README.md |
Added functional workflow section to samples overview |
|
Btw, @eavanvalkenburg and @TaoChenOSU I think it would be best to stick this functional API in to its own package. We want to get some more signal around the APIs and use of it before we deem it "GA worthy," IMO. |
We can use the experimental flag now :) |
Comment 1 (request_info in @step): Already supported. Added comment in StepWrapper.__call__ explaining why WorkflowInterrupted (BaseException) safely bypasses the except Exception handler. Comment 2 (None response): Added docstring to _get_response clarifying the (found, value) return tuple semantics and None handling. Comment 3 (bypass event type): executor_bypassed is already a dedicated event type in WorkflowEventType. Updated comment at the bypass site to make the deliberate event type choice explicit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
eavanvalkenburg
left a comment
There was a problem hiding this comment.
I focused mostly on the samples to understand the API shape and it looks good, with some questions about how certain things work!
Comment 1 (request_info in @step): Already supported. Added comment in StepWrapper.__call__ explaining why WorkflowInterrupted (BaseException) safely bypasses the except Exception handler. Comment 2 (None response): Added docstring to _get_response clarifying the (found, value) return tuple semantics and None handling. Comment 3 (bypass event type): executor_bypassed is already a dedicated event type in WorkflowEventType. Updated comment at the bypass site to make the deliberate event type choice explicit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- RunContext docstring leads with purpose (opt-in handle for HITL, custom events, state) so readers importing it from the public surface understand its role before the mechanics (#2993513452). - Rename `06_first_functional_workflow.py` to `06_functional_workflow_basics.py`; the previous filename was confusing since it followed `05_functional_workflow_with_agents.py` (#2993531979). - Simplify `05_functional_workflow_with_agents.py` to call agents directly without a @step wrapper; the step-vs-no-step contrast lives in `03-workflows/functional/agent_integration.py`, keeping the get-started sample minimal (#2993525532). - Switch functional samples to `FoundryChatClient` for consistency with the rest of 01-get-started and 03-workflows (follow-up on #2876988570). - Use walrus in `hitl_review.py` final-state assertion (#2993572182). - Add expected-output block to `basic_streaming_pipeline.py` (#2993557609). - Clarify in `parallel_pipeline.py` that `@step` composes with `asyncio.gather` (#2993597282). - `naive_group_chat.py` threads `list[Message]` between turns instead of stringifying the transcript, preserving role/authorship (#2993583231). Drive-by: pre-commit hook sorts an unrelated import block in `samples/04-hosting/foundry-hosted-agents/responses/02_local_tools/main.py`.
f7c767a to
c0093ac
Compare
|
Revived this PR:
Verified green: 2319 core tests pass (workflow suite: 748 pass, 1 skip, 2 xfail — no regressions). |
Comment 1 (request_info in @step): Already supported. Added comment in StepWrapper.__call__ explaining why WorkflowInterrupted (BaseException) safely bypasses the except Exception handler. Comment 2 (None response): Added docstring to _get_response clarifying the (found, value) return tuple semantics and None handling. Comment 3 (bypass event type): executor_bypassed is already a dedicated event type in WorkflowEventType. Updated comment at the bypass site to make the deliberate event type choice explicit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- RunContext docstring leads with purpose (opt-in handle for HITL, custom events, state) so readers importing it from the public surface understand its role before the mechanics (#2993513452). - Rename `06_first_functional_workflow.py` to `06_functional_workflow_basics.py`; the previous filename was confusing since it followed `05_functional_workflow_with_agents.py` (#2993531979). - Simplify `05_functional_workflow_with_agents.py` to call agents directly without a @step wrapper; the step-vs-no-step contrast lives in `03-workflows/functional/agent_integration.py`, keeping the get-started sample minimal (#2993525532). - Switch functional samples to `FoundryChatClient` for consistency with the rest of 01-get-started and 03-workflows (follow-up on #2876988570). - Use walrus in `hitl_review.py` final-state assertion (#2993572182). - Add expected-output block to `basic_streaming_pipeline.py` (#2993557609). - Clarify in `parallel_pipeline.py` that `@step` composes with `asyncio.gather` (#2993597282). - `naive_group_chat.py` threads `list[Message]` between turns instead of stringifying the transcript, preserving role/authorship (#2993583231). Drive-by: pre-commit hook sorts an unrelated import block in `samples/04-hosting/foundry-hosted-agents/responses/02_local_tools/main.py`.
c0093ac to
bf7d586
Compare
Comment 1 (request_info in @step): Already supported. Added comment in StepWrapper.__call__ explaining why WorkflowInterrupted (BaseException) safely bypasses the except Exception handler. Comment 2 (None response): Added docstring to _get_response clarifying the (found, value) return tuple semantics and None handling. Comment 3 (bypass event type): executor_bypassed is already a dedicated event type in WorkflowEventType. Updated comment at the bypass site to make the deliberate event type choice explicit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- RunContext docstring leads with purpose (opt-in handle for HITL, custom events, state) so readers importing it from the public surface understand its role before the mechanics (#2993513452). - Rename `06_first_functional_workflow.py` to `06_functional_workflow_basics.py`; the previous filename was confusing since it followed `05_functional_workflow_with_agents.py` (#2993531979). - Simplify `05_functional_workflow_with_agents.py` to call agents directly without a @step wrapper; the step-vs-no-step contrast lives in `03-workflows/functional/agent_integration.py`, keeping the get-started sample minimal (#2993525532). - Switch functional samples to `FoundryChatClient` for consistency with the rest of 01-get-started and 03-workflows (follow-up on #2876988570). - Use walrus in `hitl_review.py` final-state assertion (#2993572182). - Add expected-output block to `basic_streaming_pipeline.py` (#2993557609). - Clarify in `parallel_pipeline.py` that `@step` composes with `asyncio.gather` (#2993597282). - `naive_group_chat.py` threads `list[Message]` between turns instead of stringifying the transcript, preserving role/authorship (#2993583231). Drive-by: pre-commit hook sorts an unrelated import block in `samples/04-hosting/foundry-hosted-agents/responses/02_local_tools/main.py`.
c32a18e to
647019e
Compare
- Swap 05/06 get-started samples: agent workflow first (motivates why workflows exist), simple text workflow second - Rename text_pipeline → text_workflow, poem_pipeline → poem_workflow - Add @step to agent workflow sample (05) to demonstrate caching - Switch agent samples to AzureOpenAIResponsesClient with Foundry - Remove .as_agent() from agent_integration.py to focus on the key difference between inline agent calls vs @step-cached calls - Add commented-out Agent.run example in hitl_review.py - Add clarifying comment in _functional.py that event streaming is buffered (not true per-token streaming) - Add naive_group_chat.py functional sample: round-robin group chat as a plain Python loop - Update READMEs to reflect new file names and group chat sample Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. Allow request_info inside @step: Auto-inject RunContext into step functions that declare a RunContext parameter (by type or name 'ctx'), and expose get_run_context() for programmatic access. 2. Handle None responses: Log a warning when a response value is None, and document the behavior in request_info docstring. 3. Add executor_bypassed event type: Replace executor_invoked + executor_completed with a single executor_bypassed event when a step replays from cache, making cached vs live execution explicit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The three review comments (request_info in @step, None response handling, executor_bypassed event type) were already addressed in 7da7db4. This commit adds cross-cutting regression tests that exercise the interactions between these features: - HITL in step with caching: preceding step bypassed on resume - Full checkpoint lifecycle with HITL step (interrupt -> resume -> restore) - None response inside step-level request_info logs warning - WorkflowInterrupted from step does not emit executor_failed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment 1 (request_info in @step): Already supported. Added comment in StepWrapper.__call__ explaining why WorkflowInterrupted (BaseException) safely bypasses the except Exception handler. Comment 2 (None response): Added docstring to _get_response clarifying the (found, value) return tuple semantics and None handling. Comment 3 (bypass event type): executor_bypassed is already a dedicated event type in WorkflowEventType. Updated comment at the bypass site to make the deliberate event type choice explicit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mark all public classes and decorators (workflow, step, RunContext, FunctionalWorkflow, StepWrapper, FunctionalWorkflowAgent) as experimental and subject to change or removal.
- RunContext docstring leads with purpose (opt-in handle for HITL, custom events, state) so readers importing it from the public surface understand its role before the mechanics (#2993513452). - Rename `06_first_functional_workflow.py` to `06_functional_workflow_basics.py`; the previous filename was confusing since it followed `05_functional_workflow_with_agents.py` (#2993531979). - Simplify `05_functional_workflow_with_agents.py` to call agents directly without a @step wrapper; the step-vs-no-step contrast lives in `03-workflows/functional/agent_integration.py`, keeping the get-started sample minimal (#2993525532). - Switch functional samples to `FoundryChatClient` for consistency with the rest of 01-get-started and 03-workflows (follow-up on #2876988570). - Use walrus in `hitl_review.py` final-state assertion (#2993572182). - Add expected-output block to `basic_streaming_pipeline.py` (#2993557609). - Clarify in `parallel_pipeline.py` that `@step` composes with `asyncio.gather` (#2993597282). - `naive_group_chat.py` threads `list[Message]` between turns instead of stringifying the transcript, preserving role/authorship (#2993583231). Drive-by: pre-commit hook sorts an unrelated import block in `samples/04-hosting/foundry-hosted-agents/responses/02_local_tools/main.py`.
- bug_001: `ctx.request_info()` without an explicit `request_id` now derives a deterministic `auto::<index>` id from the call-counter, so HITL resume works correctly on the documented default path. A uuid was regenerated on every replay, making resume impossible. - bug_002: `StepWrapper.__call__` no longer deepcopies arguments on the cache-hit replay branch. The copy is only performed on the live-execution path (for the event log) and falls back to the original mapping if deepcopy fails, so steps whose args aren't deepcopyable (locks, sockets, sessions) can still resume from checkpoint. - bug_007: `_set_responses` now prunes each resolved `request_id` from `_pending_requests`, and the cache-hit branch in `request_info` does the same. Previously, answered requests were re-serialized into every subsequent checkpoint and the final checkpoint falsely claimed pending requests even after the workflow completed. - bug_008: `_compute_signature_hash` now mixes the function's `co_code` and `co_names` into the checkpoint signature, so changes to the workflow body invalidate older checkpoints even when steps are accessed via module / class attributes (which `_discover_step_names` can't see statically). `RunContext._record_observed_step` records observed step names for diagnostics. - bug_010: `FunctionalWorkflow.run()` docstring corrected — says "at least one of message/responses/checkpoint_id" and explicitly notes `responses` may be combined with `checkpoint_id` (the validator already allowed this). - bug_013: `FunctionalWorkflowAgent` now surfaces `request_info` events as `FunctionApprovalRequestContent` items (mirroring graph `WorkflowAgent`), threads `responses=` and `checkpoint_id=` through to the underlying workflow, and exposes `pending_requests`. Previously `.as_agent()` returned empty `AgentResponse` for HITL workflows — effectively unusable. - bug_014: `FunctionalWorkflow` now clears `_last_message`, `_last_step_cache`, and `_last_pending_request_ids` on clean completion. `run()` validates that `responses=` keys intersect the currently-pending request set (or raises with a clear error) instead of silently replaying against stale singleton state from a prior run. - bug_015: `FunctionalWorkflow.as_agent` signature now matches graph `Workflow.as_agent`: accepts `name`, `description`, `context_providers`, and `**kwargs`. `FunctionalWorkflowAgent` stores the overrides. - bug_017: `RunContext.set_state` raises `ValueError` for underscore- prefixed keys (the framework's `_step_cache` / `_original_message` keys would silently clobber user state on checkpoint save and user underscore-prefixed state was dropped on restore). Docstring documents the reserved prefix. - merged_bug_003: Workflow function arity is validated at decoration time. Multiple non-ctx parameters raise `ValueError` immediately (previously every arg past the first was silently dropped at call time). Passing a non-None `message` to a ctx-only workflow raises `ValueError` instead of silently discarding the message. Test coverage: +18 regression tests covering every fix. Full workflow suite now 766 passed, 1 skipped, 2 xfailed; full core suite 2338 passed.
- Remove dead instrumentation added in the prior commit that was never consumed: `RunContext._observed_step_names`, `RunContext._record_observed_step`, `FunctionalWorkflow._runtime_step_names`, and `FunctionalWorkflowAgent._extra_kwargs`. The signature hash relies on `co_code` alone, which covers the attribute-access case without the collection-scaffolding. - Trim over-explanatory comments that restated what the code does or what it no longer does. Keep only the comments that answer "why" for the non-obvious bits (deterministic id contract, defensive deepcopy, stale replay guard). - Compress the `_compute_signature_hash` and FunctionalWorkflow `__init__` block docstrings without losing the user-facing reasoning. Net -49 lines. Regression lock preserved (766 passed, 1 skipped, 2 xfailed).
9dad777 to
7c790b0
Compare
Previous commit incorrectly renamed the [1.1.1] header to [1.2.0], which wiped the historical 1.1.1 entries and wrongly attributed them to 1.2.0. This restores [1.1.1] to its origin/main content and adds a new [1.2.0] section above containing only the commits in python-1.1.1..HEAD: - microsoft#4238 functional workflow API - microsoft#5142 GitHub Copilot OpenTelemetry - microsoft#2403 A2A bridge support - microsoft#5070 oauth_consent_request events in Foundry clients - microsoft#5447 FoundryAgent hosted agent sessions - microsoft#5459 hosting server dependency upgrade + types - microsoft#5389 AG-UI reasoning/multimodal parsing fix - microsoft#5440 stop [TOOLBOXES] warning spam - microsoft#5455 user agent prefix fix Also corrects the [1.2.0] compare base to python-1.1.1 (not 1.1.0) and adds the missing [1.1.1] reference link.
* Bump Python package versions for 1.2.0 release Released tier bumps 1.1.1 -> 1.2.0 (core, openai, foundry, root) to reflect additive public APIs landed since 1.1.0: functional workflow API (#4238) and FunctionTool SKIP_PARSING sentinel (#5424). All beta packages stamped 1.0.0b260424, alpha packages 1.0.0a260424. All 26 non-core agent-framework-core floors raised to >=1.2.0,<2. CHANGELOG consolidates the never-tagged 1.1.1 entries with the post-merge additions into [1.2.0]. * Update CHANGELOG footer links for 1.2.0 Advance [Unreleased] comparison base from python-1.1.0 to python-1.2.0 and add a [1.2.0] reference link comparing python-1.1.0...python-1.2.0 so the heading links resolve correctly. * Fix CHANGELOG: restore [1.1.1] section and add proper [1.2.0] Previous commit incorrectly renamed the [1.1.1] header to [1.2.0], which wiped the historical 1.1.1 entries and wrongly attributed them to 1.2.0. This restores [1.1.1] to its origin/main content and adds a new [1.2.0] section above containing only the commits in python-1.1.1..HEAD: - #4238 functional workflow API - #5142 GitHub Copilot OpenTelemetry - #2403 A2A bridge support - #5070 oauth_consent_request events in Foundry clients - #5447 FoundryAgent hosted agent sessions - #5459 hosting server dependency upgrade + types - #5389 AG-UI reasoning/multimodal parsing fix - #5440 stop [TOOLBOXES] warning spam - #5455 user agent prefix fix Also corrects the [1.2.0] compare base to python-1.1.1 (not 1.1.0) and adds the missing [1.1.1] reference link.
* Sync Agent Framework Python docs for PRs since 2026-04-22 Updates docs to reflect changes in microsoft/agent-framework Python PRs merged after MicrosoftDocs/semantic-kernel-pr#982 (2026-04-22). - get-started/workflows.md, AGENTS.md: rename 01-get-started/05_first_workflow.py -> 07_first_graph_workflow.py (microsoft/agent-framework#4238). - integrations/a2a.md: add Python section 'Exposing an Agent Framework agent over A2A' documenting A2AExecutor and the new agent_framework_to_a2a.py sample (microsoft/agent-framework#2403). - agents/providers/github-copilot.md: add Observability subsection in Python zone showing configure_otel_providers() and RawGitHubCopilotAgent (microsoft/agent-framework#5142). - agents/providers/microsoft-foundry.md: add 'Connecting to a deployed (hosted) Foundry agent' subsection covering allow_preview=True / v2 and explicit AIProjectClient session creation (microsoft/agent-framework#5447). - integrations/hyperlight.md: note that result_parser does not run on the sandbox path; tools must format in-sandbox themselves (microsoft/agent-framework#5424). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Defer #4238 (functional workflow API) doc updates to PR author Revert the get-started/workflows.md sample rename and the AGENTS.md mapping update. The author of microsoft/agent-framework#4238 will handle the docs (rename + new functional workflow page) in a dedicated follow-up. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Motivation and Context
The functional API is a stepping stone between single-agent use and the full graph API. Users write workflows as plain async functions -- no executor classes, no edges, no builder patterns.
HITL resume or crash recovery
A very basic example of the functional workflow API:
Note:
@stepis opt-in for functions where per-step checkpointing matters (for example, agent calls). Without@step, workflows still support HITL and checkpointing — functions just re-execute on resume.ctx: RunContextis only needed when you use HITL (request_info), custom events (add_event), or state (get_state/set_state). Otherwise, omit it for a cleaner signature.Description
Contribution Checklist