* Emit TOOL_CALL_RESULT events on approval resume (#4589)
When a tool call is approved via the interrupt/resume flow,
_resolve_approval_responses executes the tool and injects the result
into the messages array, but no TOOL_CALL_RESULT SSE event was yielded
to the client.
Changes:
- _resolve_approval_responses now returns the list of resolved
function_result Content objects instead of None
- run_agent_stream yields ToolCallResultEvent for each resolved
approval result after RunStartedEvent is emitted
- Add ToolCallResultEvent to ag_ui.core imports in _agent_run.py
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
* fix(ag-ui): address PR review feedback for #4589
1. _resolve_approval_responses now returns only approved results (not
rejections) so TOOL_CALL_RESULT events are emitted only for executed
tools. Rejection results are still written into message history.
2. Emit resolved TOOL_CALL_RESULT events in the no-updates fallback
RUN_STARTED path so approval results are never lost.
3. Rewrite tests to use real FunctionTool with func and
approval_mode='always_require' via StubAgent default_options,
verifying actual tool execution output in TOOL_CALL_RESULT content.
Added test for rejection not emitting TOOL_CALL_RESULT.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix#4589: clean up approval resolution and add missing tests
- Extract duplicated TOOL_CALL_RESULT emission block into
_make_approval_tool_result_events helper to prevent drift
- Remove dead rejection_results construction in _resolve_approval_responses;
_replace_approval_contents_with_results already handles rejections inline
- Pass only approved_results (not all_results) to clarify the contract
- Add mixed approve/reject test validating the core splitting logic
- Add zero-updates test covering the no-updates fallback emission path
- Add direct unit test for _resolve_approval_responses return value
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
* Fix import sorting lint error in test_approval_result_event.py
Add blank line between first-party and third-party import groups
to satisfy ruff I001 rule.
Fixes#4589
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Python: Emit AG-UI events for MCP tool calls, results, and text reasoning
Fixes#4213 — `_emit_content()` in the AG-UI layer only handled `text`,
`function_call`, `function_result`, `function_approval_request`, `usage`,
and `oauth_consent_request` content types. Foundry MCP content types
(`mcp_server_tool_call`, `mcp_server_tool_result`) and `text_reasoning`
fell through unhandled, producing no SSE events for AG-UI consumers.
Added three new handler functions wired into `_emit_content()`:
- `_emit_mcp_tool_call`: emits TOOL_CALL_START + TOOL_CALL_ARGS and
tracks in FlowState for MESSAGES_SNAPSHOT inclusion
- `_emit_mcp_tool_result`: emits TOOL_CALL_END + TOOL_CALL_RESULT with
full FlowState cleanup mirroring `_emit_tool_result`
- `_emit_text_reasoning`: emits the protocol-defined reasoning event
sequence (ReasoningStart → MessageStart → MessageContent → MessageEnd
→ ReasoningEnd) with ReasoningEncryptedValueEvent for protected_data
* Add HTTP round-trip tests for MCP tool and reasoning SSE events
Exercises the full POST → SSE bytes → parse → validate pipeline for
mcp_server_tool_call, mcp_server_tool_result, text_reasoning, and
ReasoningEncryptedValueEvent content through FastAPI TestClient.
* Fix _emit_mcp_tool_result missing predictive_handler support (#4213)
- Add predictive_handler parameter to _emit_mcp_tool_result and mirror
the apply_pending_updates + StateSnapshotEvent block from _emit_tool_result
- Forward predictive_handler from _emit_content to _emit_mcp_tool_result
- Add assertion for stored arguments in MCP tool call test
- Add test for predictive handler state snapshot after MCP tool result
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
* Refactor MCP tool emit functions and add missing tests (#4213)
- Extract _emit_tool_result_common shared helper to eliminate duplication
between _emit_tool_result and _emit_mcp_tool_result
- Remove server_name prefix from tool_call_name in _emit_mcp_tool_call;
display_name now equals tool_name directly
- Add test for tool_name fallback to 'mcp_tool' when tool_name is None
- Add test for output=None fallback to empty string in _emit_mcp_tool_result
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address review feedback for #4213: review comment fixes
---------
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add script to ping on stale issues/PRs
* Add script to ping on stale issues/PRs
* Fix stale issue/PR ping script review comments
- Rename TEAM_NAME env var to TEAM_SLUG for clarity
- Add actionable error messages for 403/404 team lookup failures
- Add contents:read permission for actions/checkout
- Use github.event.inputs context with fallback for scheduled runs
- Pin PyGithub to 2.6.0 for reproducible builds
- Fetch comments once in should_ping() to reduce API calls
- Make ping() retry loop idempotent (track comment/label state)
- Validate DAYS_THRESHOLD with helpful error for non-numeric input
- Fix timezone bug: use astimezone() instead of replace(tzinfo=)
- Add comprehensive unit tests (29 tests)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Support detail field in OpenAI image_url payload (#4616)
Include the optional 'detail' field from Content.additional_properties
when building image_url payloads for the OpenAI Chat API, matching the
existing pattern used for 'filename' in document file payloads.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
* Remove reproduction report
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Simplify detail extraction from additional_properties (#4616)
- Remove unnecessary hasattr check; additional_properties is always
initialized as a dict on Content instances.
- Use 'is not None' instead of truthy check to be more precise.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
* Remove detail allowlist in chat client to align with responses client
Replace the strict allowlist check ('low', 'high', 'auto') with an
isinstance(detail, str) check so that any valid string detail value is
passed through to OpenAI. This aligns the chat client behavior with the
responses client, which passes detail through unconditionally.
Also add test coverage for:
- Future/unknown string detail values being passed through
- Data URI images (covering the 'data' branch of the match)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix FileAgentSkillsProvider accepting SkillsInstructionPrompt without {0} placeholder (#4638)
BuildSkillsInstructionPrompt validated only format-string syntax via
string.Format(template, ""), which silently accepted templates without a
{0} placeholder. The generated skills list was then dropped from the final
instructions.
Tighten validation to format with a sentinel string and verify it appears
in the output, rejecting templates that do not reference argument 0 with
an ArgumentException.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix netstandard2.0 compat and simplify prompt template validation (#4638)
- Replace string.Contains(string, StringComparison) with IndexOf for
netstandard2.0/net472 compatibility
- Remove sentinel round-trip check; validate {0} directly on the raw
template string using IndexOf
- Add positive test verifying custom SkillsInstructionPrompt with {0}
is accepted and applied to output
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix flow.interrupts overwrite when multiple tools need approval (#4590)
Change flow.interrupts assignment to append so that all interrupt entries
accumulate when multiple tools require approval in a single turn.
Both _run_common.py and _agent_run.py used assignment (=) which caused
each new interrupt to overwrite the previous one. Switching to append()
ensures RUN_FINISHED.interrupt contains all pending approvals.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add test for streaming path with multiple confirm_changes interrupts (#4590)
Add integration test exercising run_agent_stream with multiple predictive
tool calls requiring confirmation. Verifies that flow.interrupts.append()
correctly accumulates all interrupt entries and they appear in the
RUN_FINISHED event.
Also confirms FlowState already declares interrupts field with
default_factory=list, addressing the AttributeError concern from review.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
---------
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix _deduplicate_messages catch-all branch dropping valid repeated messages (#4682)
Remove the catch-all dedup branch that used (role, hash(content_str)) as a
dedup key. This incorrectly treated any two messages with the same role and
identical content as duplicates, dropping valid repeated messages (e.g., a
user saying 'yes' to confirm two separate things).
The tool-specific dedup branches (tool results by call_id, assistant tool
calls by call_id tuple) remain unchanged as they correctly identify true
protocol-level duplicates.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address review: consecutive-duplicate detection for non-tool messages (#4682)
- Replace blanket dedup removal with consecutive-duplicate detection:
only skip a message if the immediately preceding message has the same
role and content, preserving protection against upstream replays while
allowing identical messages at different conversation points.
- Strengthen test assertions to verify message identity and order, not
just list length.
- Add tests for consecutive duplicate skipping, non-consecutive
preservation, and messages with contents=None.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
* Use message_id for deduplication instead of content hashing
Deduplicate general messages by message_id when available, replacing
the consecutive-duplicate content check. Two messages with the same id
are definitively the same message (upstream replay), while identical
content with distinct ids (e.g. repeated "yes" confirmations) is
preserved. Messages without a message_id are always kept.
* Fix message_id dedup: truthy check, content-hash fallback, log safety
- Use truthy check (`if msg.message_id`) instead of `is not None` so
empty-string IDs fall through to content-hash dedup rather than
collapsing unrelated messages.
- Add content-hash fallback for messages without message_id, preventing
false negatives from integrations that don't set IDs.
- Remove raw message_id from log format string (addresses log-injection
surface with control characters).
- Add tests for empty-string message_id edge cases.
- Update existing tests to reflect content-hash dedup behavior.
Fixes#4682
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Use deepcopy for state snapshot to detect nested mutations (#4500)
Replace dict() shallow copy with copy.deepcopy() when snapshotting
workflow state before activity execution. The shallow copy shared
references to nested objects (dicts, lists), so in-place mutations by
executors were reflected in both the snapshot and live state, producing
an empty diff and preventing state updates from propagating to
downstream activities.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Python: Fix state snapshot to use deepcopy so nested mutations are detected in durable workflow activities
Fixes#4500
* Address PR review: remove report, extract testable helpers (#4500)
- Delete REPRODUCTION_REPORT.md (debugging artifact with local paths
and raw LLM output)
- Extract _create_state_snapshot() and _compute_state_updates() as
module-level helpers in _app.py so tests exercise the production
code path
- Update TestStateSnapshotDiff to import and use production helpers
instead of reimplementing snapshot/diff logic locally
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
* Add regression tests proving shallow copy bug and deep copy isolation (#4500)
Add two additional tests to TestStateSnapshotDiff:
- test_shallow_copy_would_miss_nested_mutations: reproduces the original
bug by demonstrating that dict() (shallow copy) misses nested mutations
- test_create_state_snapshot_isolates_nested_objects: verifies the
production _create_state_snapshot helper creates a true deep copy
These tests ensure a regression back to shallow copy would be caught.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add integration test exercising full activity code path (#4500)
Address PR review comment: add test_executor_activity_detects_nested_state_mutations
that captures the actual executor_activity function from _setup_executor_activity
and verifies it detects in-place nested mutations. This test would fail if
_app.py line 314 regressed from _create_state_snapshot() back to dict().
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address review feedback for #4518: review comment fixes
* Address PR review feedback for state snapshot diff
- Inline _compute_state_updates logic at call site to reuse precomputed
original_keys/current_keys sets, avoiding redundant set allocations
- Fix test docstring to describe behavioral regression instead of
hard-coding a specific line number
- Use SOURCE_ORCHESTRATOR constant in integration test instead of
literal string
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
* fix: remove unused _compute_state_updates from _app.py (#4518)
The function was inlined per review comment, making the module-level
helper unused and triggering a pyright reportUnusedFunction error.
Move the helper into the test file where it is still needed for unit
testing the diffing logic.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Validate approval responses against server-side pending request registry
* improvements
* pin GHCP sdk version to non-breaking for now
* Pin CHCP sdk to LKG.
* really fix GHCP sdk pkg version
* Fix HITL approval validation security gaps and memory leak
- Validate rejected approval responses against pending_approvals registry,
not just approved ones. Fabricated rejections without a prior request are
now stripped from messages before reaching the LLM.
- Bound _pending_approvals with OrderedDict + LRU eviction (max 10k) to
prevent unbounded memory growth from abandoned approval requests.
- Skip registration when function_call.name is None/empty; log warning
when content.id or function_call is missing at registration time.
- Document pending_approvals parameter in run_agent_stream docstring.
- Add test for fabricated rejection attack scenario.
- Assert pending approval entry is preserved after function name mismatch.
- Pre-populate pending_approvals in rejection test for correct validation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Extract function_approval_response from workflow messages (#4546)
_extract_responses_from_messages now handles function_approval_response
content in addition to function_result content. Previously, approval
responses sent via the messages field were silently dropped because the
function only checked for content.type == "function_result".
The approval response is keyed by content.id and includes the approved
status, id, and serialized function_call — consistent with how
_coerce_content identifies approval response payloads.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
* Fix#4546: Update docstring and add integration tests for message-based approvals
- Update _extract_responses_from_messages docstring to reflect that it
now handles function_approval_response content in addition to
function_result content.
- Add integration tests for run_workflow_stream across two turns with
approval responses provided via messages (function_approvals) rather
than resume.interrupts, covering both approved and denied scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address PR review feedback for #4546
- Use safer 'not .get("interrupt")' assertion instead of 'not in'
to handle Pydantic v2 model_dump() including keys with None values
- Add unit test for mixed function_result and function_approval_response
in the same message to TestExtractResponsesFromMessages
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Python: Fix `executor_completed` event with non-copyable raw_representation in mixed workflows
Fixes#4455
* fix(#4455): use class-level sets for deepcopy field exclusion
- SerializationMixin.__deepcopy__: check type(self).DEFAULT_EXCLUDE
instead of hardcoding 'raw_representation'
- Content.__deepcopy__: add _SHALLOW_COPY_FIELDS class variable and
check against it instead of hardcoding
- Fix tautological assertion in test (was always True)
- Add second excluded field to test to verify DEFAULT_EXCLUDE is
respected generically
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Decouple __deepcopy__ from DEFAULT_EXCLUDE in SerializationMixin (#4455)
Introduce _SHALLOW_COPY_FIELDS class variable in SerializationMixin to
separate deep-copy semantics from serialization semantics. Previously,
__deepcopy__ used DEFAULT_EXCLUDE to decide which fields to shallow-copy,
conflating 'not serialized' with 'not safe to deep-copy'. A field added
to DEFAULT_EXCLUDE purely for serialization (e.g. additional_properties)
would be silently shared between original and copy.
- Add _SHALLOW_COPY_FIELDS (default {'raw_representation'}) to
SerializationMixin, matching the pattern already used by Content
- Update __deepcopy__ to read from _SHALLOW_COPY_FIELDS instead of
DEFAULT_EXCLUDE
- Add test verifying DEFAULT_EXCLUDE fields are deep-copied unless
also in _SHALLOW_COPY_FIELDS
- Add test for Content._SHALLOW_COPY_FIELDS identity preservation
- Add test for ChatResponse deep-copying additional_properties
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add test for _SHALLOW_COPY_FIELDS and DEFAULT_EXCLUDE independence
Add test_deepcopy_shallow_copy_fields_override_default_exclude to verify
that a field in both DEFAULT_EXCLUDE and _SHALLOW_COPY_FIELDS is
shallow-copied (controlled by _SHALLOW_COPY_FIELDS), while a field in
DEFAULT_EXCLUDE only is still deep-copied. This addresses review comment
#11 ensuring the two class variables control independent concerns.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Remove unnecessary local variable in __deepcopy__
Inline cls._SHALLOW_COPY_FIELDS directly in the loop check instead of
assigning to a local variable first, per review feedback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Python: Exclude conversation_id from chat completions options (#4315)
When a session with service_session_id is passed to an agent using the
Chat Completions client, conversation_id leaked through _prepare_options()
into AsyncCompletions.create(), causing an 'unexpected keyword argument'
error. The Responses client already excluded conversation_id but the Chat
Completions client did not.
Added conversation_id to the exclusion set in _prepare_options().
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
* Remove reproduction report artifact
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix#4305: Handle dict chat_options in _update_conversation_id
_update_conversation_id assumed chat_options had attribute access, but
ChatOptions is a TypedDict (dict). When a dict was passed, setting
.conversation_id raised AttributeError. Now checks isinstance(dict) and
uses key access for dicts, falling back to attribute access for objects.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address PR feedback: use Mapping ABC and add missing tests (#4305)
- Use collections.abc.Mapping instead of dict for isinstance check in
_update_conversation_id, making it more robust for non-dict mapping types.
- Add test for object-style chat_options with optional options dict parameter.
- Add test verifying existing conversation_id gets overwritten (idempotent).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Remove unnecessary Mapping check in _update_conversation_id (#4305)
chat_options is always a dict, so the isinstance(chat_opts, Mapping)
check and the else branch for attribute-style access are dead code.
Simplify to direct dict key assignment and remove object-style tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Update github_copilot package for github-copilot-sdk>=0.1.32 (#4549)
- Update requires-python from >=3.10 to >=3.11
- Remove Python 3.10 classifier
- Update mypy python_version to 3.11
- Update dependency to github-copilot-sdk>=0.1.32
- Fix ToolResult API: use snake_case kwargs (text_result_for_llm,
result_type) instead of camelCase (textResultForLlm, resultType)
- Update test assertions to use attribute access on ToolResult
- Add ToolResult type assertions to tool handler tests
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix tests to use ToolInvocation dataclass instead of plain dict (#4549)
Update test_github_copilot_agent.py to pass ToolInvocation objects to tool
handlers instead of plain dicts, matching the github-copilot-sdk>=0.1.32 API
where ToolInvocation is a dataclass with an .arguments attribute.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add regression tests for ToolInvocation contract (#4549)
Add tests to lock in the new ToolInvocation-based calling convention:
- test_tool_handler_rejects_raw_dict_invocation: verifies passing a raw
dict (old calling convention) raises TypeError/AttributeError
- test_tool_handler_with_empty_arguments: verifies ToolInvocation with
empty arguments works correctly for no-arg tools
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Revert requires-python to >=3.10 to avoid breaking CI (#4549)
The repo CI runs with Python 3.10 (uv sync --all-packages) and all other
packages require >=3.10. Raising this package to >=3.11 would break the
shared install flow. The SDK dependency version constraint (>=0.1.32) will
enforce any Python version requirement from the SDK itself.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix min Python version for github_copilot package to >=3.11
github-copilot-sdk>=0.1.32 requires Python>=3.11, which conflicts
with the package's declared >=3.10 minimum, breaking uv sync.
* Bump py version for GH workflows to 3.11, exclude GHCP sdk from 3.10 items
* Fix uv command
* Fixes
* Update samples
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Improve ag-ui tests and coverage
* fix tests paths
* Fixes
* Improve AG-UI test robustness and correctness
- Map toolName → tool_call_name in SSE helpers for TOOL_CALL_START events
- Fail loudly on malformed SSE JSON in parse_sse_response() instead of silently dropping
- Detect duplicate TOOL_CALL_START/TOOL_CALL_END in assert_tool_calls_balanced()
- Remove fragile source line reference from test docstring
- Add found guard in test_client_tool_sets_additional_properties to prevent vacuous pass
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix#4321: Set CurrentUICulture to en-US in PowerFx eval()
On non-English systems, CultureInfo.CurrentUICulture causes PowerFx to
emit localized error messages. The existing ValueError guard only matches
English strings ("isn't recognized", "Name isn't valid"), so undefined
variable errors crash instead of returning None gracefully.
Fix: save and restore CurrentUICulture alongside CurrentCulture before
calling engine.eval(), ensuring error messages are always in English.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Reuse single CultureInfo instance to avoid redundant allocations
Cache CultureInfo("en-US") in a local variable instead of instantiating
it twice per eval() call, as suggested in PR review.
Fixes#4321
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add assertion for CurrentUICulture restoration after eval
Assert that the production code's finally-block correctly restores
CurrentUICulture to it-IT after eval returns, covering future
regressions where the culture could leak.
The CultureInfo caching suggestion (comment #2) was already
implemented in the production code.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix#4371: Propagate session to manager agent in StandardMagenticManager
StandardMagenticManager._complete() was calling self._agent.run(messages)
without passing a session. This caused context providers (e.g.
RedisHistoryProvider) configured on the manager agent to silently fail,
as each call created a new ephemeral session with a different session_id.
Changes:
- Create an AgentSession in StandardMagenticManager.__init__()
- Pass session=self._session in _complete() calls to agent.run()
- Persist/restore the session in checkpoint save/restore methods
- Add regression tests for session propagation and checkpoint round-trip
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add type: ignore[reportPrivateUsage] to private attribute assertions in tests
Address PR review feedback: add # type: ignore[reportPrivateUsage] comments
to _session attribute accesses in the new regression tests, matching the
existing convention used elsewhere in test_magentic.py (e.g., lines 401-406).
The @pytest.mark.asyncio decorator is not needed because pyproject.toml
sets asyncio_mode = "auto".
Fixes#4371
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address review: use getattr for private _session access in tests (#4371)
Replace direct mgr._session access with getattr(mgr, "_session") to avoid
reportPrivateUsage type-checking warnings without needing type: ignore comments.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
* Address PR review: fix session restore guard and improve test robustness (#4371)
- Use 'is not None' instead of truthiness check for session_payload restore
- Use getattr() for private _session attribute access in tests
- Add backward-compatibility test for on_checkpoint_restore with empty state
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Make non-async tests plain def to avoid pytest-asyncio dependency (#4409)
Tests that never await anything don't need to be async. Using plain def
ensures they always run regardless of pytest-asyncio configuration.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix IndexError when reasoning models return no text content (#4384)
In _prepare_message_for_openai(), the text_reasoning case unconditionally
accessed all_messages[-1] to attach reasoning_details. When a reasoning
model (e.g. gpt-5-mini) returns reasoning_details without text content,
all_messages is empty, causing an IndexError.
Guard the access by initializing all_messages with the current args dict
when it is empty, so reasoning_details can be safely attached.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address review: buffer reasoning details for valid message payloads (#4384)
- Buffer pending reasoning details and attach to the next message with
content/tool_calls, avoiding standalone reasoning-only messages.
- When reasoning is the only content, emit a message with empty content
to satisfy Chat Completions schema requirements.
- Strengthen test assertions to verify text+reasoning co-location and
that all messages with reasoning_details also have content or tool_calls.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix text_reasoning handling: always buffer and tighten tests (#4384)
- Always buffer reasoning into pending_reasoning instead of conditionally
attaching to the previous message via fragile all_messages emptiness check
- Attach buffered reasoning to last message at end-of-loop when no subsequent
content consumed it
- Assert exact content values (content == '' not in ('', None))
- Assert exact list lengths (== 1 not >= 1) for stronger regression guards
- Add test for reasoning before FunctionCallContent
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Python: Add regression tests for #3948 - Entry JoinExecutor initializes Workflow.Inputs
Add tests verifying that when workflow.run() is called with a dict or string
input, the Entry node (JoinExecutor with kind: 'Entry') correctly initializes
Workflow.Inputs via _ensure_state_initialized so that:
- Expressions like =inputs.age resolve to the correct value
- Conditions like =Local.age < 13 evaluate based on actual input (not blank/0)
- String inputs populate both inputs.input and System.LastMessage.Text
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pre-commit auto-fixes
* Fix D420 and RUF070 lint errors across packages
* Revert _workflow.py yield-inside-context-manager changes
Moving yield inside `with _framework_event_origin()` blocks in the
async generator causes ContextVar token reset failures on Python 3.12
Windows. The token stays un-reset while the generator is suspended,
and async generator finalization in a different contextvars.Context
triggers ValueError, corrupting OpenTelemetry span state and causing
test_span_creation_and_attributes to see leaked spans.
Keep yields outside the context manager blocks to ensure tokens are
reset immediately before the generator suspends.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix walrus operator precedence for model_id in AzureOpenAIResponsesClient (#4299)
Add parentheses around the walrus assignment so model_id receives the
actual string value instead of the boolean result of
`kwargs.pop(...) and not deployment_name`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address review: replace walrus with explicit None check, add edge-case tests (#4299)
- Replace walrus operator with explicit assignment and 'is not None'
check to avoid boolean-coercion pitfalls (empty string now correctly
surfaces as ValueError instead of silently falling back)
- Add test: deployment_name takes precedence over model_id kwarg
- Add test: model_id='' raises ValueError
- Add test: model_id=None falls back to env var
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add explicit validation for empty model_id in AzureOpenAIResponsesClient
Reject empty or whitespace-only model_id with ValueError instead of
silently passing an empty deployment name downstream. This ensures the
test_init_model_id_kwarg_empty_string test correctly validates behavior
defined in production code rather than relying on downstream validation.
Addresses PR review feedback for #4299.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Simplify model_id handling using walrus operator
Addresses review comment on PR #4310.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Restore explicit model_id validation to fix test failures (#4299)
The walrus operator refactor silently dropped the empty-string validation,
causing test_init_model_id_kwarg_empty_string to fail. Restore the explicit
None check and ValueError raise for empty model_id.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Revert "Restore explicit model_id validation to fix test failures (#4299)"
This reverts commit 1d2965fff6.
* Revert to walrus operator fix per review feedback
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Python: Fix Executor handler type checking with __future__ annotations (#3898)
Use typing.get_type_hints() in _validate_handler_signature to resolve
string annotations from `from __future__ import annotations`. This
mirrors the fix applied to FunctionExecutor in #2308.
When __future__ annotations are enabled, type annotations are stored as
strings. The handler decorator was passing these strings directly to
validate_workflow_context_annotation, which uses typing.get_origin and
returns None for strings, causing a ValueError.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address PR review feedback for #3898: improve error handling and test coverage
- Wrap typing.get_type_hints() in try/except to provide a descriptive
ValueError mentioning the handler name when annotations cannot be resolved
- Strengthen bare context test to assert output_types and workflow_output_types
- Add test for @handler(input=..., output=...) with future annotations
covering the skip_message_annotation branch
- Add test for union-type context annotations with future annotations
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Narrow exception catch and add test for unresolvable annotations (#3898)
- Narrow except clause from bare Exception to (NameError, AttributeError,
TypeError) to avoid masking unexpected errors.
- Add test_handler_unresolvable_annotation_raises to verify that a handler
with a forward-reference to a non-existent type raises ValueError with
the expected message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix#3898: fall back to raw annotations when get_type_hints fails
When typing.get_type_hints(func) raises NameError (unresolvable forward
ref), AttributeError, RecursionError, or any other exception, fall back
to the raw parameter annotations instead of raising a ValueError.
This matches the suggestion from @moonbox3 on PR #4317.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix test to match new fallback behavior when get_type_hints fails (#3898)
The code now falls back to raw string annotations instead of raising
'Failed to resolve type annotations'. A ValueError is still raised when
the raw string ctx annotation is not a valid WorkflowContext type, so
update the test to match on ValueError without checking the message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Apply pyupgrade: remove unnecessary string annotation quote
* Add noqa for intentionally undefined name in annotation test
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix _merge_options dropping dict-defined tools (#4303)
_merge_options used getattr(tool, 'name', None) to de-duplicate tools,
which returns None for dict-style tool definitions. This caused all
override dict tools to be treated as duplicates of each other and of any
base dict tools, silently dropping them.
Add _get_tool_name() helper that extracts the name from both object-style
tools (via .name attribute) and dict-style tools (via tool['function']['name']).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address review: fix None dedup bug and add comprehensive tests (#4303)
- Exclude None from existing_names set so nameless/malformed tools are
not silently deduplicated against each other
- Add test for cross-type dedup (dict tool + object tool with same name)
- Add test verifying nameless tools are preserved (not falsely deduped)
- Add unit tests for _get_tool_name edge cases: missing function key,
non-dict function value, missing name, no name attribute, non-dict
inputs, and valid dict/object tools
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix OpenAIResponsesClient mishandling single-tool inputs (#4304)
Use normalize_tools() in _prepare_tools_for_openai to wrap single tools
(FunctionTool or dict) in a list before iteration, consistent with the
chat client implementation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address PR review feedback for #4304
- Use precise type annotation matching normalize_tools/OpenAIChatClient signature
instead of collapsed Sequence[Any] | Any | None
- Move emptiness guard after normalize_tools() call so single falsy tool
objects are not silently swallowed
- Import ToolTypes for the type annotation
- Expand test_prepare_tools_for_openai_single_function_tool assertions to
verify parameters, strict, and parameter schema fields
- Add test_prepare_tools_for_openai_none to verify None input returns []
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
WorkflowAgent._run_impl() and _run_stream_impl() did not set
session_context._response before calling _run_after_providers().
This caused InMemoryHistoryProvider.after_run() to see context.response
as None, so response messages were never stored in the session.
On subsequent runs, the workflow only received prior user inputs without
assistant responses, breaking multi-turn conversations.
Fix: Set session_context._response to the workflow result before running
after_run providers, matching the behavior of the regular Agent class.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
During Assistants API streaming, TextDeltaBlock.text.annotations was
ignored when creating Content objects. This caused raw placeholder
strings like 【4:0†source】 to pass through to downstream consumers
(including AG-UI) instead of being resolved to citation metadata.
Map FileCitationDeltaAnnotation and FilePathDeltaAnnotation from
delta_block.text.annotations to Annotation objects on the Content,
consistent with the existing patterns in _responses_client.py and
_chat_client.py.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(python): preserve workflow run kwargs on response continuation (#4293)
When continuing a paused workflow with run(responses=...), the existing
run kwargs stored in state were unconditionally overwritten with an empty
dict. This caused subsequent agent invocations to lose the original run
context (e.g., custom_data, user tokens).
Now kwargs are only overwritten when:
- New kwargs are explicitly provided (override), or
- State was just cleared for a fresh run (initialize to {})
On continuation without new kwargs, existing kwargs are preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address PR review feedback for #4293
- Use consistent get_state(key, {}) default pattern in _agent_executor.py
and _workflow_executor.py instead of get_state(key) or {} to safely
handle missing WORKFLOW_RUN_KWARGS_KEY
- Add test for empty-value kwargs on continuation (custom_data={}) to
verify the is-not-None boundary between overwrite and preserve
- Add test for reset_context=True with no kwargs to exercise the elif
branch that initializes WORKFLOW_RUN_KWARGS_KEY to {}
- Add len assertion to override test for consistency
- Document kwargs-collapsing behavior at the public API call site
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Python: Strip reserved kwargs in AgentExecutor to prevent collision (#4295)
workflow.run(session=...) passed 'session' through to agent.run() via
**run_kwargs while AgentExecutor also passes session=self._session
explicitly, causing TypeError: got multiple values for keyword argument.
_prepare_agent_run_args now strips reserved params (session, stream,
messages) from run_kwargs and logs a warning when they are present.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address PR review feedback for #4295
- Use _RESERVED_RUN_PARAMS constant in stripping loop instead of
hardcoded tuple to maintain single source of truth
- Trim frozenset to only stripped keys (session, stream, messages);
options and additional_function_arguments have separate merge logic
- Fix caplog type annotation to use TYPE_CHECKING pattern
- Assert options return value in reserved-kwarg stripping test
- Add test for multiple reserved kwargs supplied simultaneously
- Add integration test for messages= kwarg via workflow.run()
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
HandoffBuilder.participants() accepted SupportsAgentRun by API contract,
but build() failed at runtime because _prepare_agent_with_handoffs()
requires Agent instances for cloning, tool injection, and middleware.
Fix: Update all public type hints, docstrings, and validation in
HandoffBuilder and HandoffAgentExecutor to require Agent explicitly.
The isinstance check is now performed early in participants() with a
clear error message explaining why Agent is required.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Python: Fix AgentResponse.value being None when streaming workflow (#3970)
The streaming path in BaseAgent.run() used the raw 'options' parameter
(passed by the caller) to bind response_format into the outer stream's
finalizer. When response_format was set in default_options rather than
runtime options, it was missing from the finalizer and value was None.
Fix: Use the merged chat_options from the run context (via ctx_holder),
matching the non-streaming path which already uses ctx['chat_options'].
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address review feedback for #3970: safer ctx access, add test coverage
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix Workflow.as_agent() streaming regression in ag-ui
* Address PR feedback
* workflows wip
* wip
* wip
* Workflow AG-UI demo
* Fixes for handoff workflow demo
* Fixes to workflows support in AG-UI
* Fixes
* Add headers to some demo files
* Fix comment
* Fixes for store
* Make _input_schema lazy-loaded
* fix mypy
* revert session change to handoff only for now
---------
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
* Support specifying types via handler and executor decorators
* Add handling for string types
* Fix typing
* Address PR feedback
* All or nothing for handler typing approach
* Fix mypy issues
* type support for request info
* Fix naming issue
* Fix mypy
In _prepare_options(), the 'instructions' key was excluded from run_options
but never re-added. This caused instructions passed via as_agent(instructions=...)
to be silently dropped, making agents in sequential workflows ignore their
configured instructions.
Fixes#3507
* fix(ag-ui): execute tools after approval in human-in-the-loop flow
* Fix shared state bug
* Bug fix finalized
* Refactoring to clean up code
* Code cleanup
* More fixes
* More code cleanup
* Add version detection in __init__.py to ruff ignore list
* Support HITL for orchestration patterns
* Cleanup around naming
* Fix typing issues
* Clean up
* Naming clean up
* Updates to HITL to make it cleaner
* Rename human input hook to orchestration request info
* Clean up per PR feedback
* Provide way for HITL with magentic
* support tool call approvals and hitl stall replan
* human plan intervention sample
* Clean up
* Improve loging
* updates
* Add type annotations for AgentRunEvent and AgentRunUpdateEvent data attributes
* Fix unnecessary cast after typing improvement
* Mcp pkg update introduced type change. Fix it.
* update opentelemetry deps to 1.39.0 and use LogRecordExporter for type compatibility