Skip to content

Commit cba7880

Browse files
Add session store feature usage telemetry
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3e5c81ad-75e8-4e92-a883-8bbd676c6c8c
1 parent 85703d6 commit cba7880

7 files changed

Lines changed: 45 additions & 2 deletions

File tree

docs/decisions/0033-python-session-store-serialization.md renamed to docs/decisions/0034-python-session-store-serialization.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ implementations.
199199
`InMemorySessionStore`, protocol, or ABC is introduced. `agent-framework-hosting` consumes the core type and no longer
200200
owns or re-exports `SessionStore` (this will be a breaking change in the `hosting` package).
201201

202+
Actual `SessionStore` and `FileSessionStore` operations mark Python feature-usage index 17,
203+
`core.session_store`, following ADR-0033's use-not-presence policy. Construction and import alone do not mark the bit.
204+
202205
`SessionStore` accepts opaque non-empty keys so custom backends can use their native key contracts. `FileSessionStore`
203206
accepts opaque keys up to 128 characters and encodes values that are not portable filename stems; this supports
204207
provider IDs such as `telegram:<bot-id>:<chat-id>` without permitting path traversal. `AgentState` remains

‎docs/specs/feature-usage-bit-registry.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@ only to approved first-party endpoints.
135135
| 14 | `core.file_skills_source` | File-backed skills | `agent_framework.FileSkillsSource` |
136136
| 15 | `core.in_memory_skills_source` | In-memory / programmatic skills | `agent_framework.InMemorySkillsSource` |
137137
| 16 | `core.mcp_skills_source` | MCP-backed skills | `agent_framework.MCPSkillsSource` |
138-
| 17–31 | _reserved_ | core growth | — |
138+
| 17 | `core.session_store` | Agent session store | `agent_framework.SessionStore` / `FileSessionStore` |
139+
| 18–31 | _reserved_ | core growth | — |
139140
| 32 | `orchestration.sequential` | Sequential orchestration | `agent_framework_orchestrations.SequentialBuilder` |
140141
| 33 | `orchestration.concurrent` | Concurrent orchestration | `agent_framework_orchestrations.ConcurrentBuilder` |
141142
| 34 | `orchestration.group_chat` | Group-chat orchestration | `agent_framework_orchestrations.GroupChatBuilder` |

‎python/packages/core/agent_framework/_sessions.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,6 +1509,7 @@ async def get(self, session_id: str) -> AgentSession | None:
15091509
Raises:
15101510
ValueError: If ``session_id`` is empty.
15111511
"""
1512+
mark_feature_used(FeatureIndex.CORE_SESSION_STORE)
15121513
SessionStore.validate_session_id(session_id)
15131514
session = self._sessions.get(session_id)
15141515
return copy.deepcopy(session) if session is not None else None
@@ -1523,6 +1524,7 @@ async def set(self, session_id: str, session: AgentSession) -> None:
15231524
Raises:
15241525
ValueError: If ``session_id`` is empty.
15251526
"""
1527+
mark_feature_used(FeatureIndex.CORE_SESSION_STORE)
15261528
SessionStore.validate_session_id(session_id)
15271529
self._sessions[session_id] = copy.deepcopy(session)
15281530

@@ -1535,6 +1537,7 @@ async def delete(self, session_id: str) -> None:
15351537
Raises:
15361538
ValueError: If ``session_id`` is empty.
15371539
"""
1540+
mark_feature_used(FeatureIndex.CORE_SESSION_STORE)
15381541
SessionStore.validate_session_id(session_id)
15391542
self._sessions.pop(session_id, None)
15401543

@@ -1611,6 +1614,7 @@ def __init__(
16111614

16121615
async def get(self, session_id: str) -> AgentSession | None:
16131616
"""Load a session snapshot, or return ``None`` when it does not exist."""
1617+
mark_feature_used(FeatureIndex.CORE_SESSION_STORE)
16141618
file_path = self._session_file_path(session_id)
16151619
file_lock = self._session_file_lock(file_path)
16161620

@@ -1664,6 +1668,7 @@ def _read() -> AgentSession | None:
16641668

16651669
async def set(self, session_id: str, session: AgentSession) -> None:
16661670
"""Persist a session snapshot atomically."""
1671+
mark_feature_used(FeatureIndex.CORE_SESSION_STORE)
16671672
file_path = self._session_file_path(session_id)
16681673
file_lock = self._session_file_lock(file_path)
16691674
if type(session) is not AgentSession:
@@ -1696,6 +1701,7 @@ def _write() -> None:
16961701

16971702
async def delete(self, session_id: str) -> None:
16981703
"""Delete a persisted session snapshot, if present."""
1704+
mark_feature_used(FeatureIndex.CORE_SESSION_STORE)
16991705
file_path = self._session_file_path(session_id)
17001706
file_lock = self._session_file_lock(file_path)
17011707

‎python/packages/core/agent_framework/_telemetry.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class FeatureIndex(IntEnum):
5353
CORE_FILE_SKILLS_SOURCE = 14
5454
CORE_IN_MEMORY_SKILLS_SOURCE = 15
5555
CORE_MCP_SKILLS_SOURCE = 16
56+
CORE_SESSION_STORE = 17
5657

5758

5859
# This environment variable is reserved by the Foundry hosting environment to

‎python/packages/core/tests/core/test_sessions.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from dataclasses import dataclass
1010
from pathlib import Path
1111
from typing import Any
12+
from unittest.mock import patch
1213

1314
import msgspec
1415
import pytest
@@ -37,6 +38,7 @@
3738
_filter_approval_control_messages,
3839
is_local_history_conversation_id,
3940
)
41+
from agent_framework._telemetry import FeatureIndex
4042
from agent_framework.exceptions import MiddlewareException
4143

4244
# ---------------------------------------------------------------------------
@@ -924,8 +926,36 @@ async def test_empty_session_id_raises(self) -> None:
924926
with pytest.raises(ValueError, match="session_id"):
925927
await store.delete("")
926928

929+
@pytest.mark.parametrize("operation", ["get", "set", "delete"])
930+
async def test_operations_mark_feature_usage(self, operation: str) -> None:
931+
store = SessionStore()
932+
933+
with patch("agent_framework._sessions.mark_feature_used") as mark_feature_used_mock:
934+
if operation == "get":
935+
await store.get("session-1")
936+
elif operation == "set":
937+
await store.set("session-1", AgentSession())
938+
else:
939+
await store.delete("session-1")
940+
941+
mark_feature_used_mock.assert_called_once_with(FeatureIndex.CORE_SESSION_STORE)
942+
927943

928944
class TestFileSessionStore:
945+
@pytest.mark.parametrize("operation", ["get", "set", "delete"])
946+
async def test_operations_mark_feature_usage(self, tmp_path: Path, operation: str) -> None:
947+
store = FileSessionStore(tmp_path)
948+
949+
with patch("agent_framework._sessions.mark_feature_used") as mark_feature_used_mock:
950+
if operation == "get":
951+
await store.get("session-1")
952+
elif operation == "set":
953+
await store.set("session-1", AgentSession())
954+
else:
955+
await store.delete("session-1")
956+
957+
mark_feature_used_mock.assert_called_once_with(FeatureIndex.CORE_SESSION_STORE)
958+
929959
async def test_round_trips_session_across_store_instances(self, tmp_path: Path) -> None:
930960
session = AgentSession(session_id="framework-session", service_session_id={"response_id": "resp-1"})
931961
session.state["nested"] = {

‎python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ def _resolve_checkpoint_root(is_hosted: bool) -> str:
405405
pass
406406

407407
return "/home/session/.checkpoints"
408+
408409
def __init__(
409410
self,
410411
agent: SupportsAgentRun,

‎python/packages/foundry_hosting/tests/test_responses.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5037,7 +5037,7 @@ def _patched_isinstance(obj: Any, cls: Any) -> bool:
50375037
):
50385038
server = ResponsesHostServer(mock_agent, store=InMemoryResponseProvider())
50395039

5040-
checkpoint_path = server._checkpoint_storage_path
5040+
checkpoint_path = server._checkpoint_storage_path
50415041
assert checkpoint_path is not None
50425042
actual_normalized = checkpoint_path.replace("\\", "/")
50435043
assert actual_normalized.endswith("/home/testuser/.checkpoints")
@@ -5075,4 +5075,5 @@ def test_hosted_with_unusable_home_falls_back_to_default(
50755075
assert server._checkpoint_storage_path == "/home/session/.checkpoints"
50765076
assert server._checkpoint_storage_path != "/.checkpoints"
50775077

5078+
50785079
# endregion

0 commit comments

Comments
 (0)