From 450c6692a919bba2420a99c0ab73a36d37604241 Mon Sep 17 00:00:00 2001 From: Anas Khan <83116240+anxkhn@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:13:21 +0530 Subject: [PATCH 1/2] fix: avoid nil session dereference in OpenAI local mode The OpenAI agent executor built by KAgentApp.build_local() is constructed with session_factory=None, so execute() streams events with session=None. _stream_agent_events then built SessionContext(session_id=session.session_id) unconditionally, raising AttributeError before the agent ran and failing every local-mode A2A request with "'NoneType' object has no attribute 'session_id'". Fall back to the A2A context id when there is no session, matching the session identifier already used elsewhere in this executor. Add regression tests that exercise execute() with session_factory=None. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com> --- .../src/kagent/openai/_agent_executor.py | 5 +- .../tests/test_agent_executor.py | 64 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 python/packages/kagent-openai/tests/test_agent_executor.py diff --git a/python/packages/kagent-openai/src/kagent/openai/_agent_executor.py b/python/packages/kagent-openai/src/kagent/openai/_agent_executor.py index d1711fb23b..bc4c916538 100644 --- a/python/packages/kagent-openai/src/kagent/openai/_agent_executor.py +++ b/python/packages/kagent-openai/src/kagent/openai/_agent_executor.py @@ -110,7 +110,10 @@ async def _stream_agent_events( ) -> None: """Stream agent execution events and convert them to A2A events.""" task_result_aggregator = TaskResultAggregator() - session_context = SessionContext(session_id=session.session_id) + # In local mode there is no session_factory, so session is None; fall back to + # the A2A context id, which is the session identifier used elsewhere here. + session_id = session.session_id if session else context.context_id + session_context = SessionContext(session_id=session_id) try: # Use run_streamed for streaming support diff --git a/python/packages/kagent-openai/tests/test_agent_executor.py b/python/packages/kagent-openai/tests/test_agent_executor.py new file mode 100644 index 0000000000..51a25c1af6 --- /dev/null +++ b/python/packages/kagent-openai/tests/test_agent_executor.py @@ -0,0 +1,64 @@ +"""Tests for OpenAIAgentExecutor session handling.""" + +import uuid +from unittest.mock import AsyncMock, MagicMock, patch + +from a2a.server.agent_execution.context import RequestContext +from a2a.server.events.event_queue import EventQueue +from a2a.types import Message, Part, Role, TextPart + +from kagent.openai._agent_executor import OpenAIAgentExecutor + + +def _make_context() -> MagicMock: + context = MagicMock(spec=RequestContext) + context.message = Message( + message_id=str(uuid.uuid4()), + role=Role.user, + parts=[Part(TextPart(text="hello"))], + ) + context.current_task = None + context.task_id = "task-1" + context.context_id = "ctx-1" + context.get_user_input.return_value = "hello" + return context + + +def _make_streamed_result() -> MagicMock: + async def _stream_events(): + for _ in (): + yield None + + result = MagicMock() + result.stream_events = _stream_events + result.final_output = "done" + return result + + +class TestLocalModeSession: + """The executor built by build_local() has session_factory=None.""" + + @patch("kagent.openai._agent_executor.Runner.run_streamed") + async def test_execute_without_session_factory_does_not_raise(self, mock_run_streamed): + mock_run_streamed.return_value = _make_streamed_result() + + executor = OpenAIAgentExecutor(agent=MagicMock(), app_name="test", session_factory=None) + context = _make_context() + event_queue = AsyncMock(spec=EventQueue) + + await executor.execute(context, event_queue) + + mock_run_streamed.assert_called_once() + + @patch("kagent.openai._agent_executor.Runner.run_streamed") + async def test_session_context_falls_back_to_context_id(self, mock_run_streamed): + mock_run_streamed.return_value = _make_streamed_result() + + executor = OpenAIAgentExecutor(agent=MagicMock(), app_name="test", session_factory=None) + context = _make_context() + event_queue = AsyncMock(spec=EventQueue) + + await executor.execute(context, event_queue) + + session_context = mock_run_streamed.call_args.kwargs["context"] + assert session_context.session_id == context.context_id From b06d54526c3fb6f07d0f592ab1f67ca197bddff8 Mon Sep 17 00:00:00 2001 From: Anas Khan <83116240+anxkhn@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:12:41 +0530 Subject: [PATCH 2/2] fix: align local OpenAI session identifiers Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com> --- .../src/kagent/openai/_agent_executor.py | 4 +--- .../kagent-openai/tests/test_agent_executor.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/python/packages/kagent-openai/src/kagent/openai/_agent_executor.py b/python/packages/kagent-openai/src/kagent/openai/_agent_executor.py index bc4c916538..86e10f5c45 100644 --- a/python/packages/kagent-openai/src/kagent/openai/_agent_executor.py +++ b/python/packages/kagent-openai/src/kagent/openai/_agent_executor.py @@ -110,9 +110,7 @@ async def _stream_agent_events( ) -> None: """Stream agent execution events and convert them to A2A events.""" task_result_aggregator = TaskResultAggregator() - # In local mode there is no session_factory, so session is None; fall back to - # the A2A context id, which is the session identifier used elsewhere here. - session_id = session.session_id if session else context.context_id + session_id = session.session_id if session else getattr(context, "session_id", None) or context.context_id session_context = SessionContext(session_id=session_id) try: diff --git a/python/packages/kagent-openai/tests/test_agent_executor.py b/python/packages/kagent-openai/tests/test_agent_executor.py index 51a25c1af6..e6f00afbf5 100644 --- a/python/packages/kagent-openai/tests/test_agent_executor.py +++ b/python/packages/kagent-openai/tests/test_agent_executor.py @@ -20,6 +20,7 @@ def _make_context() -> MagicMock: context.current_task = None context.task_id = "task-1" context.context_id = "ctx-1" + context.session_id = None context.get_user_input.return_value = "hello" return context @@ -62,3 +63,17 @@ async def test_session_context_falls_back_to_context_id(self, mock_run_streamed) session_context = mock_run_streamed.call_args.kwargs["context"] assert session_context.session_id == context.context_id + + @patch("kagent.openai._agent_executor.Runner.run_streamed") + async def test_session_context_uses_context_session_id(self, mock_run_streamed): + mock_run_streamed.return_value = _make_streamed_result() + + executor = OpenAIAgentExecutor(agent=MagicMock(), app_name="test", session_factory=None) + context = _make_context() + context.session_id = "session-1" + event_queue = AsyncMock(spec=EventQueue) + + await executor.execute(context, event_queue) + + session_context = mock_run_streamed.call_args.kwargs["context"] + assert session_context.session_id == context.session_id