Python: Fix CopilotStudioAgent to reuse conversation ID from existing session#5299
Python: Fix CopilotStudioAgent to reuse conversation ID from existing session#5299giles17 wants to merge 2 commits intomicrosoft:mainfrom
Conversation
…rosoft#5285) CopilotStudioAgent unconditionally called _start_new_conversation() in both _run_impl and _run_stream_impl, ignoring any existing service_session_id on the session. Add a guard to only start a new conversation when there is no existing service_session_id, matching the pattern used by other agents. Also fix pre-existing pyright reportMissingImports errors for orjson in file_history_provider samples. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
giles17
left a comment
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 93%
✓ Correctness
The fix correctly guards
_start_new_conversation()behindif not session.service_session_id:in both_run_impland_run_stream_impl, matching the pattern used by other agents (GitHub Copilot, Claude).AgentSession.service_session_iddefaults toNone(confirmed in_sessions.py:179), so the falsy check works correctly. The two new tests properly verify that an existingservice_session_idis preserved andstart_conversationis not called. Theorjsontype-ignore comments in the sample files are harmless cosmetic changes. No correctness issues found.
✓ Security Reliability
The fix is correct and well-tested. It adds a simple guard (
if not session.service_session_id) before calling_start_new_conversation()in both the streaming and non-streaming paths, preventing the agent from discarding an existing conversation ID. Theservice_session_idattribute onAgentSessiondefaults toNone(confirmed at _sessions.py:739), so the truthiness check is appropriate. Two new tests properly verify thatstart_conversationis not called when a session already has a conversation ID. Theorjsontype-ignore comments in the sample files are harmless cosmetic changes. No security or reliability concerns.
✓ Test Coverage
The test coverage for the bug fix is solid. Two new tests (
test_run_reuses_existing_conversationandtest_run_streaming_reuses_existing_conversation) properly verify both the non-streaming and streaming paths. The assertions are meaningful: they check thatstart_conversationis not called, thatask_questionreceives the existing conversation ID, and that the session ID is preserved. The existing tests continue to cover the new-conversation path (whereservice_session_idstarts asNone). No blocking issues found.
✓ Design Approach
The core fix is correct and well-motivated: adding
if not session.service_session_id:guards in both_run_impland_run_stream_implmatches the pattern used by every other agent in the framework and is backed by solid tests. The new tests verify both non-streaming and streaming session reuse paths, check thatstart_conversationis not called, and confirm the existing ID is forwarded toask_question. No design issues with the main change. A minor issue exists in the sample file changes:# type: ignore[reportMissingImports]is not valid syntax for either mypy (which uses error codes likeimport) or pyright (which uses# pyright: ignore[...]). The repo's own convention, as seen in other sample files, is# pyright: ignore[...], so the suppression comment as written is effectively a no-op.
Suggestions
- Consider adding a multi-turn integration-style test that calls
runtwice on the same session: first without aservice_session_id(triggering_start_new_conversation), then a second time verifying the session reuses the ID from the first call. This would exercise the full lifecycle and catch any mutation issues between calls. - The
# type: ignore[reportMissingImports]comment added in both sample files uses invalid mixed syntax —reportMissingImportsis a pyright code but# type: ignoreis mypy syntax. Neither tool will recognize this. Change to# pyright: ignore[reportMissingImports]to match project convention and actually suppress the warning.
Automated review by giles17's agents
There was a problem hiding this comment.
Pull request overview
Fixes Copilot Studio multi-turn behavior by preserving an existing AgentSession.service_session_id (conversation ID) across repeated run() calls, enabling server-side conversation continuity (Fixes #5285).
Changes:
- Guarded
_start_new_conversation()in both non-streaming and streaming execution paths so it only runs whensession.service_session_idis unset. - Added regression tests verifying
start_conversationis skipped andask_questionreceives the existing conversation ID when a session already has one. - Updated two conversation sample files to suppress optional
orjsonimport typing errors.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py | Reuses existing service_session_id instead of always starting a new conversation. |
| python/packages/copilotstudio/tests/test_copilot_agent.py | Adds regression tests for conversation ID reuse in both streaming and non-streaming paths. |
| python/samples/02-agents/conversations/file_history_provider.py | Adds typing ignore for optional orjson import in sample. |
| python/samples/02-agents/conversations/file_history_provider_conversation_persistence.py | Adds typing ignore for optional orjson import in sample. |
Remove unrelated orjson type-ignore comment changes from sample files that were outside the scope of the conversation-ID reuse fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Motivation and Context
CopilotStudioAgent._run_impland_run_stream_implunconditionally called_start_new_conversation()on everyrun()invocation, overwriting the session'sservice_session_ideven when a persistent session was passed in. This made multi-turn conversations impossible because each call started a fresh server-side conversation with no memory of prior turns.Fixes #5285
Description
The root cause was that both
_run_impland_run_stream_implalways replacedsession.service_session_idwith a new conversation ID, ignoring any existing value. The fix adds a guard (if not session.service_session_id) so that_start_new_conversation()is only called when the session does not already carry a conversation ID, allowing calers to reuse the same session across multiplerun()calls for continuous dialog. Regression tests verify that both the non-streaming and streaming paths skipstart_conversationand pass the existing conversation ID toask_questionwhen a session already has aservice_session_id.Contribution Checklist