Skip to content

Commit 8e3ea0d

Browse files
feat: Unify session management across MCP codebase (#538)
* feat: Phase 1 - Add data storage capabilities to unified session system Enhance utils/session/ with workflow data storage: - Add `data` field to Session model for key-value storage - Add get(), set(), delete(), clear_data() methods to Session - Update FileSystemSessionStorage to persist data field to data.json - Update MemorySessionStorage to handle data field - Add comprehensive tests for data storage functionality - Update serialization (to_dict/from_dict) to include data field This is Phase 1 of the session unification plan, enabling the session system to support both conversation tracking and workflow data storage. All 60 session tests pass including 6 new data storage tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * feat: Phase 2 - Migrate utils/agent/ to use SessionManager Replace in-memory agent sessions with unified SessionManager: - Add SessionManager integration to SpecializedAgent - Create _create_storage() method for storage backend selection - Use FileSystemSessionStorage when session_storage_path is set - Use MemorySessionStorage otherwise (backward compatible) - Update _get_session_history() to convert ConversationMessage to dict - Update _add_to_history() to use add_conversation_message() - Update clear_session_history() to use SessionManager API - Update get_session_history() to return same format as before - Update set_session() to ensure session exists - Fix test for _sessions attribute (now _session_manager) - Fix test with invalid filesystem path to use temp directory All 98 agent tests pass + 24 ExploreAgent tests pass. Agents now support both memory and filesystem persistence! 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * feat: Phase 3 - Delete old automation session module Remove plugins/automation/runtime_data/session.py: - Delete SessionData, SessionStorage, get_storage() module - Update runtime_data/__init__.py to remove session exports - Add note directing users to utils.session.SessionManager - No breaking changes: module was not yet in use All 89 passing automation tests still pass. 4 pre-existing workflow engine test failures unchanged. This completes the session unification migration. The codebase now has a single unified session system in utils/session/ supporting: - Conversation tracking (for agents and tools) - Key-value data storage (for workflows) - Metadata and statistics - Multiple storage backends (memory, filesystem) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * docs: Update session unification plan with completion summary Mark plan as completed and add implementation summary: - All 3 phases completed successfully in ~1 hour - 247 tests passing (60 session + 98 agent + 89 automation) - ~160 lines of duplicate code deleted - Zero breaking changes - New capabilities: persistent agent sessions, unified data storage Phase 1 (30 min): Enhanced utils/session/ with data storage Phase 2 (20 min): Migrated utils/agent/ to SessionManager Phase 3 (10 min): Deleted automation session module Key achievements: ✅ Single unified session system across entire codebase ✅ Agents support both memory and filesystem persistence ✅ Sessions support conversation tracking + data storage ✅ No backward compatibility issues Much faster than estimated 1.5-2 weeks due to: - Experimental automation plugin allowed direct deletion - Well-tested foundation (54 existing session tests) - Clean internal separation in agent implementation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent ef891e0 commit 8e3ea0d

File tree

10 files changed

+1559
-190
lines changed

10 files changed

+1559
-190
lines changed

docs/session_unification_plan.md

Lines changed: 1280 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
11
"""
22
Runtime Data Module
33
4-
Manages runtime data for automation workflows, including:
4+
Manages runtime data for automation workflows:
55
- State: Structural context (WorkflowContext, StepResult, StepStatus)
6-
- Session: Session-level storage and persistence
6+
7+
Note: Session management has been unified with utils.session.
8+
Use utils.session.SessionManager for session-level storage.
79
"""
810

911
# State management (structural context)
1012
from .state import StepResult, StepStatus, WorkflowContext
1113

12-
# Session storage
13-
from .session import SessionData, SessionStorage, get_storage
14-
1514
__all__ = [
1615
# State
1716
"StepResult",
1817
"StepStatus",
1918
"WorkflowContext",
20-
# Session
21-
"SessionData",
22-
"SessionStorage",
23-
"get_storage",
2419
]

plugins/automation/runtime_data/session.py

Lines changed: 0 additions & 156 deletions
This file was deleted.

utils/agent/agent.py

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from .cli_executor import CLIExecutor, CLIConfig, CLIType
1515
from .system_prompt import SystemPromptBuilder
16+
from utils.session import SessionManager, FileSystemSessionStorage, MemorySessionStorage, SessionStorage
1617

1718
logger = logging.getLogger(__name__)
1819

@@ -87,7 +88,30 @@ def __init__(self, config: AgentConfig):
8788
"""
8889
self.config = config
8990
self._executor = CLIExecutor(config.to_cli_config())
90-
self._sessions: Dict[str, List[Dict[str, str]]] = {}
91+
92+
# Use session manager instead of raw dict
93+
storage = self._create_storage(config)
94+
self._session_manager = SessionManager(storage)
95+
96+
# Ensure session exists
97+
session_id = self._get_session_id()
98+
if not self._session_manager.storage.session_exists(session_id):
99+
self._session_manager.create_session(
100+
session_id=session_id,
101+
purpose=f"{self.__class__.__name__} session"
102+
)
103+
104+
def _create_storage(self, config: AgentConfig) -> SessionStorage:
105+
"""Create storage backend based on config."""
106+
if config.session_storage_path:
107+
# Use filesystem storage for persistence
108+
return FileSystemSessionStorage(
109+
sessions_dir=config.session_storage_path,
110+
history_dir=config.session_storage_path.parent / ".history"
111+
)
112+
else:
113+
# Use memory storage (backward compatible)
114+
return MemorySessionStorage()
91115

92116
@abstractmethod
93117
def get_system_prompt(self) -> str:
@@ -176,14 +200,23 @@ def _get_session_storage_path(self) -> Path:
176200
def _get_session_history(self) -> List[Dict[str, str]]:
177201
"""Get the message history for the current session"""
178202
session_id = self._get_session_id()
179-
if session_id not in self._sessions:
180-
self._sessions[session_id] = []
181-
return self._sessions[session_id]
203+
session = self._session_manager.get_session(session_id)
204+
if session is None:
205+
return []
206+
# Convert ConversationMessage to dict format
207+
return [
208+
{"role": msg.role, "content": msg.content}
209+
for msg in session.conversation
210+
]
182211

183212
def _add_to_history(self, role: str, content: str):
184213
"""Add a message to the session history"""
185-
history = self._get_session_history()
186-
history.append({"role": role, "content": content})
214+
session_id = self._get_session_id()
215+
self._session_manager.add_conversation_message(
216+
session_id=session_id,
217+
role=role,
218+
content=content
219+
)
187220

188221
def _build_prompt(
189222
self,
@@ -307,8 +340,10 @@ def clear_session_history(self, session_id: Optional[str] = None):
307340
session_id: Session to clear (if None, clears current session)
308341
"""
309342
sid = session_id or self._get_session_id()
310-
if sid in self._sessions:
311-
self._sessions[sid] = []
343+
session = self._session_manager.get_session(sid)
344+
if session:
345+
session.conversation.clear()
346+
self._session_manager.storage.save_session(session)
312347
logger.info(f"Cleared history for session {sid}")
313348

314349
def get_session_history(self, session_id: Optional[str] = None) -> List[Dict[str, str]]:
@@ -322,7 +357,14 @@ def get_session_history(self, session_id: Optional[str] = None) -> List[Dict[str
322357
List of message dictionaries with 'role' and 'content' keys
323358
"""
324359
sid = session_id or self._get_session_id()
325-
return self._sessions.get(sid, []).copy()
360+
session = self._session_manager.get_session(sid)
361+
if session is None:
362+
return []
363+
# Convert ConversationMessage to dict format
364+
return [
365+
{"role": msg.role, "content": msg.content}
366+
for msg in session.conversation
367+
]
326368

327369
def set_session(self, session_id: str):
328370
"""
@@ -332,6 +374,12 @@ def set_session(self, session_id: str):
332374
session_id: ID of the session to switch to
333375
"""
334376
self.config.session_id = session_id
377+
# Ensure session exists
378+
if not self._session_manager.storage.session_exists(session_id):
379+
self._session_manager.create_session(
380+
session_id=session_id,
381+
purpose=f"{self.__class__.__name__} session"
382+
)
335383
logger.info(f"Switched to session {session_id}")
336384

337385
def __repr__(self) -> str:

utils/agent/tests/test_agent.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,10 @@ def test_init(self):
122122
assert agent.config == config
123123
assert agent._executor is not None
124124
assert isinstance(agent._executor, CLIExecutor)
125-
assert agent._sessions == {}
125+
assert agent._session_manager is not None
126+
# Verify session was created
127+
session_id = agent._get_session_id()
128+
assert agent._session_manager.storage.session_exists(session_id)
126129

127130
def test_get_system_prompt(self):
128131
"""Test getting system prompt"""

utils/agent/tests/test_agent_system_prompt_integration.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -130,20 +130,24 @@ def test_agent_session_storage_path_default(self):
130130

131131
def test_agent_session_storage_path_custom(self):
132132
"""Test agent with custom session storage path"""
133-
custom_path = Path("/custom/sessions")
134-
config = AgentConfig(
135-
cli_type=CLIType.COPILOT,
136-
session_id="custom_path_test",
137-
session_storage_path=custom_path,
138-
)
139-
140-
agent = DefaultSystemPromptAgent(config)
141-
142-
# Get storage path
143-
storage_path = agent._get_session_storage_path()
144-
145-
# Should use custom path + session_id
146-
assert storage_path == custom_path / "custom_path_test"
133+
import tempfile
134+
with tempfile.TemporaryDirectory() as tmpdir:
135+
custom_path = Path(tmpdir) / "sessions"
136+
config = AgentConfig(
137+
cli_type=CLIType.COPILOT,
138+
session_id="custom_path_test",
139+
session_storage_path=custom_path,
140+
)
141+
142+
agent = DefaultSystemPromptAgent(config)
143+
144+
# Get storage path
145+
storage_path = agent._get_session_storage_path()
146+
147+
# Should use custom path + session_id
148+
assert storage_path == custom_path / "custom_path_test"
149+
# Verify session directory was created
150+
assert custom_path.exists()
147151

148152
def test_agent_default_session_id(self):
149153
"""Test agent with default session ID"""

utils/session/models.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ class Session:
116116
metadata: SessionMetadata
117117
invocation_ids: List[str] = field(default_factory=list)
118118
conversation: List[ConversationMessage] = field(default_factory=list)
119+
# NEW: Workflow data storage
120+
data: Dict[str, Any] = field(default_factory=dict)
119121

120122
def add_invocation(self, invocation_id: str, duration_ms: Optional[float] = None):
121123
"""Link an invocation to this session"""
@@ -144,12 +146,34 @@ def add_message(
144146
self.conversation.append(message)
145147
self.metadata.updated_at = datetime.now()
146148

149+
# NEW: Data storage methods (from automation/session.py)
150+
def get(self, key: str, default: Any = None) -> Any:
151+
"""Get value from session data."""
152+
return self.data.get(key, default)
153+
154+
def set(self, key: str, value: Any) -> None:
155+
"""Set value in session data."""
156+
self.data[key] = value
157+
self.metadata.updated_at = datetime.now()
158+
159+
def delete(self, key: str) -> None:
160+
"""Delete value from session data."""
161+
if key in self.data:
162+
del self.data[key]
163+
self.metadata.updated_at = datetime.now()
164+
165+
def clear_data(self) -> None:
166+
"""Clear all session data (preserves conversation)."""
167+
self.data.clear()
168+
self.metadata.updated_at = datetime.now()
169+
147170
def to_dict(self) -> Dict[str, Any]:
148171
"""Convert session to dictionary for serialization"""
149172
return {
150173
"metadata": self.metadata.to_dict(),
151174
"invocation_ids": self.invocation_ids,
152175
"conversation": [msg.to_dict() for msg in self.conversation],
176+
"data": self.data,
153177
}
154178

155179
@classmethod
@@ -161,4 +185,5 @@ def from_dict(cls, data: Dict[str, Any]) -> "Session":
161185
conversation=[
162186
ConversationMessage.from_dict(msg) for msg in data.get("conversation", [])
163187
],
188+
data=data.get("data", {}),
164189
)

0 commit comments

Comments
 (0)