Skip to content

Python: Feature/hosted dwf - #5531

Merged
Evan Mattson (moonbox3) merged 17 commits into
mainfrom
feature/hosted-dwf
Apr 29, 2026
Merged

Python: Feature/hosted dwf#5531
Evan Mattson (moonbox3) merged 17 commits into
mainfrom
feature/hosted-dwf

Conversation

@alliscode

Copy link
Copy Markdown
Member

This pull request enhances support for passing conversation history into declarative workflow agents, ensuring that workflows receive the full message context (not just the latest user turn) when triggered via agents. It updates the input initialization logic, executor signatures, and hosting integration to handle lists of Message objects, and adds a regression test to guarantee correct population of System.LastMessageText for compatibility with existing YAML workflows.

alliscodeand others added 4 commits April 27, 2026 12:53
…rt executor
The declarative start executor (JoinExecutor) only advertised dict and str
in its input_types, so WorkflowAgent.__init__ rejected it with
'Workflow's start executor cannot handle list[Message]'.
Add list[Message] to the JoinExecutor handler annotation and add a
matching branch in DeclarativeActionExecutor._ensure_state_initialized
that extracts the last user-message text and falls through to the
string-input initialization path, so =System.LastMessageText works
end-to-end via as_agent().
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When Workflow.as_agent() is invoked with a list[Message], the start executor now populates Conversation.messages / Conversation.history / System.conversations.{id}.messages with prior turns only (excluding the latest user message), and surfaces the latest user message via Inputs.input and System.LastMessage*. This matches InvokeAzureAgent's contract that the messages binding holds prior turns and the executor itself appends the new user input before invoking, avoiding double-append of the trailing user turn while preserving full history (incl. assistant/system/tool roles and multi-modal content) for downstream actions.
MessageRole and other str-subclass Enums passed isinstance(v, str) and were forwarded to pythonnet unchanged. pythonnet then raised 'MessageRole value cannot be converted to System.String' for every PowerFx primitive when ConditionGroup/Expr eval walked the symbol table containing Conversation.messages. Reduce Enum members to their underlying value before the primitive check so eval sees plain strings/ints.
_handle_inner_workflow only forwarded the latest user turn to WorkflowAgent.run, even though _handle_inner_agent already prepends history fetched from Foundry storage to the messages it sends a regular agent. Declarative workflows reset Conversation.messages on every run (state.initialize), so checkpoint replay alone does not give them prior turns - the host has to pass them in, the same way it does for non-workflow agents. Mirror that contract: fetch context.get_history() and pass [*history, *input_messages] to the workflow agent.
CopilotAI review requested due to automatic review settings April 28, 2026 00:28
@moonbox3Evan Mattson (moonbox3) added the python Usage: [Issues, PRs], Target: Python label Apr 28, 2026
@github-actionsgithub-actionsBot changed the title Feature/hosted dwfPython: Feature/hosted dwfApr 28, 2026
@moonbox3

Evan Mattson (moonbox3) commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework/_workflows
_agent.py3587180%66, 74–80, 116–117, 210, 271, 284, 351, 362, 364, 424, 430, 447–448, 455, 457, 463, 525–526, 535, 543, 569, 602–604, 606, 608, 610, 615, 620, 674, 704, 721, 760–763, 769, 775, 779–780, 783–789, 793–794, 802, 863, 870, 876–877, 888, 920, 927, 948, 957, 961, 963–965, 972
_runner.py174298%309–310
_workflow.py2781993%91, 270–272, 274–275, 293, 297, 461, 676, 697, 747, 759, 765, 770, 790–792, 843
packages/declarative/agent_framework_declarative/_workflows
_declarative_base.py4625887%47, 50, 154, 242, 256, 270, 281, 298, 425–426, 433–434, 449, 479, 562–564, 566–568, 570, 573, 580, 606, 632–638, 640–641, 643–651, 672–673, 675–676, 966–969, 993–994, 1001–1002, 1007, 1016–1017, 1023
_executors_control_flow.py170696%82, 90, 153, 343–344, 348
packages/foundry_hosting/agent_framework_foundry_hosting
_responses.py52811877%151–152, 161–162, 166, 171, 185, 209, 235–237, 256–258, 260–262, 264–266, 270–273, 285–291, 301–302, 320–322, 327, 329, 336, 338–339, 341, 343, 349–352, 354–356, 360, 363, 368–374, 377–378, 380–381, 389–394, 466–467, 1112–1114, 1116, 1163–1164, 1166–1167, 1169–1170, 1172–1173, 1178, 1187, 1190–1192, 1194, 1212, 1215, 1250–1254, 1258–1264, 1271–1275, 1281–1287, 1294, 1300, 1303
TOTAL30502356088%

Python Unit Test Overview

TestsSkippedFailuresErrorsTime
613130 💤0 ❌0 🔥1m 36s ⏱️

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves hosted declarative workflow agents so they receive full conversation context (history + current input) when invoked via agent interfaces, aligning behavior with non-workflow agents and maintaining YAML compatibility via System.LastMessageText.

Changes:

  • Foundry hosting now prepends stored conversation history to workflow-agent inputs before invoking WorkflowAgent.run(...).
  • Declarative workflow state initialization now accepts list[Message], populating Conversation.* and System.LastMessage* accordingly, and improves PowerFx safety by coercing Enum values.
  • Adds a regression test ensuring Workflow.as_agent() populates System.LastMessageText correctly.
Show a summary per file
FileDescription
python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.pyPasses Foundry-stored history + current request messages into workflow agent runs.
python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.pyAdds list[Message] trigger initialization and Enum-to-primitive coercion for PowerFx symbol conversion.
python/packages/declarative/agent_framework_declarative/_workflows/_executors_control_flow.pyUpdates JoinExecutor trigger type to include list[Message] so workflows can be exposed as agents.
python/packages/declarative/tests/test_workflow_factory.pyAdds regression coverage for Workflow.as_agent() and System.LastMessageText.

Copilot's findings

  • Files reviewed: 4/4 changed files
  • Comments generated: 2

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 4 | Confidence: 89% | Result: All clear

Reviewed: Correctness, Security Reliability, Test Coverage, Design Approach


Automated review by alliscode's agents

alliscodeand others added 2 commits April 28, 2026 08:49
…urn continuation
Allow Workflow.run(message=..., checkpoint_id=...) so callers can restore
prior workflow state from a checkpoint AND deliver a new message to the
start executor in a single call. The existing reset_context logic
already preserves shared state when checkpoint_id is set, so this gives
us 'fresh start executor invocation with prior state intact' - exactly
what hosted multi-turn declarative workflows need.
- _workflow.py: drop the message+checkpoint_id mutual exclusion and
update _execute_with_message_or_checkpoint to do both (restore then
execute) when both are provided.
- _agent.py: in _run_core's checkpoint branch, also forward
input_messages so WorkflowAgent.run(messages, checkpoint_id=...) works
end-to-end. Falls back to the legacy 'restore only' behavior when
messages are absent.
- _declarative_base.py: detect continuation in _ensure_state_initialized
by checking whether DECLARATIVE_STATE_KEY already exists in shared
state; if so, refresh inputs/LastMessage* and append non-user trigger
messages instead of calling state.initialize() (which would wipe
Conversation/Local/System).
- foundry_hosting/_responses.py: collapse the host's two-call pattern
(restore-only, then fresh run) into a single combined call now that
the underlying APIs support it.
- tests: drop the assertion that combined message+checkpoint_id raises.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the prior 'combined message + checkpoint_id in one run()' approach
with a cleaner default: Workflow.run no longer wipes shared state or runner-
context messages between calls. Iteration counting and per-run kwargs still
reset on a fresh-message run; checkpoint and responses runs are continuations
that preserve everything.
This lets a WorkflowAgent be invoked repeatedly on the same instance and
maintain multi-turn context (e.g. accumulated Conversation.messages) without
asking developers to opt in. Hosted-agent multi-turn pattern becomes two
explicit calls: restore-from-checkpoint (drive to idle), then run-with-message.
Key changes:
- _workflow.py: drop _state.clear() and reset_for_new_run() from run().
Reset iteration count and run kwargs on fresh-message runs only.
Restore 'Cannot provide both message and checkpoint_id' validation.
Add async guard: fresh-message run with un-drained pending executor
messages from a prior run is invalid.
- _runner.py: clear _state before import_state in restore_from_checkpoint
so restore is authoritative (import_state merges, not replaces).
- _agent.py: revert checkpoint branch to restore-only (no message forward).
- _responses.py (foundry_hosting): two-call host pattern - restore checkpoint
silently, then run with new user input.
- tests: state-preservation is the new default; rebuild Workflow for clean slate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- _workflow.py: collapse nested if (SIM102), drop redundant assignment (RET504)
- _declarative_base.py: remove unused last_user_msg = tail assignment
whose Message | None type clashed with the prior Message-typed branch
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 8/8 changed files
  • Comments generated: 4

alliscodeand others added 2 commits April 28, 2026 11:27
- _declarative_base.py: continuation branch was writing 'Inputs.input' via
state.set, which routes to the Custom namespace and never updates the
PowerFx-visible Workflow.Inputs.input. Update state_data['Inputs'] in
place via get_state_data / set_state_data so =Workflow.Inputs.input and
=inputs.input see the new turn's user text on continuation.
- _declarative_base.py: refresh docstring to clarify that on a list[Message]
trigger, Conversation.messages excludes the current user message at the
start of the turn (agent executors append it before invoking the inner
agent).
- _responses.py: when previous_response_id is supplied (no conversation_id),
the prior checkpoint lives under <storage>/<previous_response_id> but new
checkpoints must land under <storage>/<current_response_id> for the next
turn to find them. Hold onto restore_storage from the get_latest lookup
and pass it to the restore-only run; pass write_storage (current id) to
the message-delivery run and to checkpoint cleanup.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace state._state.get(...) protected access with new public
is_initialized() method on DeclarativeWorkflowState (also clearer intent
for the continuation detection use case).
- Add narrow pyright ignores for the Any-typed trigger paths that pyright
cannot fully narrow (the list[Message] isinstance loop and the
fallback-DefaultTransform branch).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@alliscode
Ben Thomas (alliscode) marked this pull request as ready for review April 28, 2026 18:45

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 1 | Confidence: 95%

✓ Design Approach

The patch fixes the immediate continuation bugs, but two parts of the new design still mis-handle multi-turn state. In declarative workflows, the continuation path now replays the entire incoming session history back into already-restored Conversation.* state, so chat history will grow with duplicates on every turn. In foundry hosting, the new unconditional restore-only call can leave WorkflowAgent.pending_requests populated, causing the subsequent user-message call to be interpreted as request responses instead of a fresh turn. Both issues stem from layering new behavior on top of existing stateful abstractions without accounting for what those layers already persist.


Automated review by alliscode's agents

alliscodeand others added 5 commits April 28, 2026 12:43
* Add Workflow.reset() public method as recovery escape hatch when an
in-flight run aborted (e.g. WorkflowConvergenceException) and the
workflow is not checkpointed. Update the in-flight messages guard's
error message to point callers at it.
* Add test_workflow_run_inflight_messages_guard exercising both the
guard (sync + streaming) and the reset() recovery path.
* Add test_workflow_reset_rejects_concurrent_runs to lock down the
in-progress guard on reset.
* Add test_as_agent_continuation_preserves_prior_state covering the
is_continuation branch in _ensure_state_initialized: stamps a marker
between calls and asserts it survives, while Inputs.input and
System.LastMessageText refresh to the new turn.
* Add test_powerfx_safe.py regression tests for the Enum branch in
_make_powerfx_safe (str-subclass, int-subclass, plain Enum, and
Enums nested in dict/list).
* Drop redundant @pytest.mark.asyncio on
test_as_agent_round_trip_with_last_message_text (asyncio_mode='auto').
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address Copilot review on _responses.py: the restore-only checkpoint
replay populates self._agent.pending_requests for any request_info
events captured in the checkpoint. The follow-up run(input_messages)
call would then route through WorkflowAgent._process_pending_requests,
which expects function-response content and rejects plain text input
as 'unexpected content while awaiting request info responses'.
Workflows resumed from a checkpoint that was idle-with-pending-requests
would therefore fail every subsequent plain-text user turn. Inspect the
loaded checkpoint and skip the pre-pass when its
pending_request_info_events dict is non-empty. Workflows that don't use
request_info (the current sample set) are unaffected; workflows that do
will fall through to a fresh-message run rather than silently corrupting
the routing state.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The exact-version pins on azure-ai-agentserver-{core,responses,invocations}
forced foundry-hosting consumers to upgrade in lockstep with every beta
bump from upstream. Switch to '>=current,<next-major' so we pick up patch
and feature updates within the same major series without a coordinated
release.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The in-flight-messages guard prevented silent misbehavior, but the
companion Workflow.reset() escape hatch only cleared _messages while
leaving iteration count, executor-local state, and shared State
mutations in an indeterminate condition after a mid-run failure. That
gave a false sense of recovery.
Recovery from a mid-run failure is supported only via checkpoint
restoration. Keep the guard and reframe its error message accordingly;
remove reset() and its tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve foundry_hosting/pyproject.toml conflict by keeping the loosened
azure-ai-agentserver-* pins (>=X.Y.ZbN,<NEXT_MAJOR) while taking main's
agent-framework-core>=1.2.1,<2 bump.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadpython/packages/core/agent_framework/_workflows/_workflow.py Outdated
Comment threadpython/packages/core/agent_framework/_workflows/_workflow.py Outdated
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pythonUsage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@alliscode@moonbox3@TaoChenOSU