From 9d846b636a4e000a57e41e1ea6938363047b685b Mon Sep 17 00:00:00 2001 From: xusenquan <1320208646@qq.com> Date: Wed, 5 Aug 2026 18:38:02 +0800 Subject: [PATCH 1/3] feat(runtime): add session_storage_mount_path to storage_config Add session_storage_mount_path field to StorageConfig, alongside sfs_turbo, so users can specify a session storage mount path in the container when creating/deploying a runtime. --- .../toolkit/operations/runtime/deploy.py | 9 +++------ .../toolkit/operations/runtime/init.py | 1 + src/agentarts/toolkit/utils/runtime/config.py | 20 ++++++++++++------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/agentarts/toolkit/operations/runtime/deploy.py b/src/agentarts/toolkit/operations/runtime/deploy.py index 8c3e9182..80cddb64 100644 --- a/src/agentarts/toolkit/operations/runtime/deploy.py +++ b/src/agentarts/toolkit/operations/runtime/deploy.py @@ -114,11 +114,7 @@ def create_agentarts_runtime( if runtime_cfg.storage_config: sc = runtime_cfg.storage_config.to_dict() - # to_dict() returns {"sfs_turbo": [{...}]} (list) or {} (unconfigured). - # Only enforce required fields when the user explicitly opted - # into SFS Turbo by providing sfs_turbo_id. An unset/all-null - # storage_config (e.g. from `agentarts init` / `agentarts config`) - # is treated as "not configured" so deploy is never blocked. + # Validate sfs_turbo: mount_path is required when sfs_turbo_id is set. st_list = sc.get("sfs_turbo") or [] if st_list: st = st_list[0] @@ -126,7 +122,8 @@ def create_agentarts_runtime( if not st.get("mount_path"): msg = "storage_config.sfs_turbo.mount_path is required when sfs_turbo_id is set" raise ValueError(msg) - storage_config = sc + if sc: + storage_config = sc if runtime_cfg.environment_variables: env_vars = [{"key": kv.key, "value": kv.value} for kv in runtime_cfg.environment_variables if kv.value] diff --git a/src/agentarts/toolkit/operations/runtime/init.py b/src/agentarts/toolkit/operations/runtime/init.py index 2630cb72..a8a09779 100644 --- a/src/agentarts/toolkit/operations/runtime/init.py +++ b/src/agentarts/toolkit/operations/runtime/init.py @@ -239,6 +239,7 @@ def create_config_file( sfs_path: null mount_path: null # required when using SFS Turbo read_only: false + session_storage_mount_path: null # Session storage mount path in the container environment_variables:{env_vars_yaml} diff --git a/src/agentarts/toolkit/utils/runtime/config.py b/src/agentarts/toolkit/utils/runtime/config.py index 59a27158..260b9c57 100644 --- a/src/agentarts/toolkit/utils/runtime/config.py +++ b/src/agentarts/toolkit/utils/runtime/config.py @@ -232,6 +232,10 @@ class StorageConfig(BaseModel): default_factory=SfsTurboConfig, description="SFS Turbo storage configuration", ) + session_storage_mount_path: str | None = Field( + default=None, + description="Session storage mount path in the container", + ) model_config = { "extra": "allow", @@ -242,15 +246,17 @@ def to_dict(self) -> dict[str, Any]: The API expects ``sfs_turbo`` as an **array** of config objects (not a single object). This method wraps the single config object - in a list. Returns ``{}`` when SFS is not configured (no sfs_turbo_id). + in a list. Returns ``{}`` when nothing is configured. """ + result: dict[str, Any] = {} st = self.sfs_turbo - if st is None or not st.sfs_turbo_id: - return {} - item = st.to_dict() - if not item: - return {} - return {"sfs_turbo": [item]} + if st is not None and st.sfs_turbo_id: + item = st.to_dict() + if item: + result["sfs_turbo"] = [item] + if self.session_storage_mount_path is not None: + result["session_storage_mount_path"] = self.session_storage_mount_path + return result class NetworkConfig(BaseModel): From fd2030f5dd3719e84f9d400ff3c70f0156f579f5 Mon Sep 17 00:00:00 2001 From: xusenquan <1320208646@qq.com> Date: Thu, 6 Aug 2026 11:11:23 +0800 Subject: [PATCH 2/3] test(runtime): add unit tests for session_storage_mount_path --- .../toolkit/operations/runtime/test_config.py | 26 +++++++ .../toolkit/operations/runtime/test_deploy.py | 72 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/tests/unit/toolkit/operations/runtime/test_config.py b/tests/unit/toolkit/operations/runtime/test_config.py index 4d2ed72b..b2dc2a5a 100644 --- a/tests/unit/toolkit/operations/runtime/test_config.py +++ b/tests/unit/toolkit/operations/runtime/test_config.py @@ -588,6 +588,32 @@ def test_storage_config_default_to_dict_is_empty(self): """A default StorageConfig (no sfs_turbo_id) serializes to {}.""" assert StorageConfig().to_dict() == {} + def test_storage_config_to_dict_session_storage_only(self): + """StorageConfig with only session_storage_mount_path serializes correctly.""" + cfg = StorageConfig( + session_storage_mount_path="/home/user/sessions", + ) + result = cfg.to_dict() + assert result == {"session_storage_mount_path": "/home/user/sessions"} + + def test_storage_config_to_dict_both_fields(self): + """StorageConfig with both sfs_turbo and session_storage_mount_path.""" + cfg = StorageConfig( + sfs_turbo=SfsTurboConfig( + sfs_turbo_id="12345678-1234-1234-1234-123456789012", + mount_path="/data", + ), + session_storage_mount_path="/home/user/sessions", + ) + result = cfg.to_dict() + assert result == { + "sfs_turbo": [{ + "sfs_turbo_id": "12345678-1234-1234-1234-123456789012", + "mount_path": "/data", + }], + "session_storage_mount_path": "/home/user/sessions", + } + def test_invalid_sfs_turbo_id_rejected(self): """A non-UUID sfs_turbo_id is rejected by validation.""" import pytest diff --git a/tests/unit/toolkit/operations/runtime/test_deploy.py b/tests/unit/toolkit/operations/runtime/test_deploy.py index 6ea8ac87..a88d0a89 100644 --- a/tests/unit/toolkit/operations/runtime/test_deploy.py +++ b/tests/unit/toolkit/operations/runtime/test_deploy.py @@ -221,6 +221,78 @@ def test_storage_config_forwarded(self, mock_client, tmp_path, monkeypatch): }] } + @patch("agentarts.toolkit.operations.runtime.deploy.RuntimeClient") + def test_storage_config_session_storage_only_forwarded(self, mock_client, tmp_path, monkeypatch): + """session_storage_mount_path alone is forwarded without sfs_turbo.""" + monkeypatch.chdir(tmp_path) + + mock_client_instance = MagicMock() + mock_client.return_value = mock_client_instance + mock_client_instance.create_or_update_agent.return_value = { + "id": "agent-123", + "latest_version": "v1", + } + + agent_config = AgentArtsConfig( + runtime=AgentArtsRuntimeConfig( + storage_config=StorageConfig( + session_storage_mount_path="/home/user/sessions", + ), + ), + ) + + create_agentarts_runtime( + agent_name="test-agent", + swr_image="swr.cn-north-4.myhuaweicloud.com/org/repo:latest", + region="cn-north-4", + agent_config=agent_config, + ) + + call_args = mock_client_instance.create_or_update_agent.call_args + assert call_args.kwargs["storage_config"] == { + "session_storage_mount_path": "/home/user/sessions", + } + + @patch("agentarts.toolkit.operations.runtime.deploy.RuntimeClient") + def test_storage_config_both_fields_forwarded(self, mock_client, tmp_path, monkeypatch): + """Both sfs_turbo and session_storage_mount_path are forwarded together.""" + monkeypatch.chdir(tmp_path) + + mock_client_instance = MagicMock() + mock_client.return_value = mock_client_instance + mock_client_instance.create_or_update_agent.return_value = { + "id": "agent-123", + "latest_version": "v1", + } + + agent_config = AgentArtsConfig( + runtime=AgentArtsRuntimeConfig( + storage_config=StorageConfig( + sfs_turbo=SfsTurboConfig( + sfs_turbo_id="12345678-1234-1234-1234-123456789012", + mount_path="/data", + ), + session_storage_mount_path="/home/user/sessions", + ), + ), + ) + + create_agentarts_runtime( + agent_name="test-agent", + swr_image="swr.cn-north-4.myhuaweicloud.com/org/repo:latest", + region="cn-north-4", + agent_config=agent_config, + ) + + call_args = mock_client_instance.create_or_update_agent.call_args + assert call_args.kwargs["storage_config"] == { + "sfs_turbo": [{ + "sfs_turbo_id": "12345678-1234-1234-1234-123456789012", + "mount_path": "/data", + }], + "session_storage_mount_path": "/home/user/sessions", + } + @patch("agentarts.toolkit.operations.runtime.deploy.RuntimeClient") def test_storage_config_requires_mount_path_when_id_set(self, mock_client, tmp_path, monkeypatch): """When sfs_turbo_id is set, mount_path is required (returns None).""" From 192de697e540295d09838830a7b140293e6d7b6b Mon Sep 17 00:00:00 2001 From: xusenquan <1320208646@qq.com> Date: Wed, 12 Aug 2026 16:23:42 +0800 Subject: [PATCH 3/3] refactor(sdk): change session_storage_mount_path to nested session_storage object Replace flat string field session_storage_mount_path with SessionStorageConfig model containing mount_path, serialized as {"session_storage": {"mount_path": "..."}}. --- .../toolkit/operations/runtime/init.py | 3 +- src/agentarts/toolkit/utils/runtime/config.py | 30 +++++++++++++++---- .../toolkit/operations/runtime/test_config.py | 13 ++++---- .../toolkit/operations/runtime/test_deploy.py | 13 ++++---- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/agentarts/toolkit/operations/runtime/init.py b/src/agentarts/toolkit/operations/runtime/init.py index a8a09779..8f6e7472 100644 --- a/src/agentarts/toolkit/operations/runtime/init.py +++ b/src/agentarts/toolkit/operations/runtime/init.py @@ -239,7 +239,8 @@ def create_config_file( sfs_path: null mount_path: null # required when using SFS Turbo read_only: false - session_storage_mount_path: null # Session storage mount path in the container + session_storage: + mount_path: null # Session storage mount path in the container environment_variables:{env_vars_yaml} diff --git a/src/agentarts/toolkit/utils/runtime/config.py b/src/agentarts/toolkit/utils/runtime/config.py index 260b9c57..93dc4b48 100644 --- a/src/agentarts/toolkit/utils/runtime/config.py +++ b/src/agentarts/toolkit/utils/runtime/config.py @@ -225,6 +225,24 @@ def to_dict(self) -> dict[str, Any]: return {k: v for k, v in data.items() if v not in ([], {})} +class SessionStorageConfig(BaseModel): + """Session storage configuration.""" + + mount_path: str | None = Field( + default=None, + description="Session storage mount path in the container", + ) + + model_config = { + "extra": "allow", + } + + def to_dict(self) -> dict[str, Any]: + """Convert configuration to dictionary.""" + data = self.model_dump(mode="json", exclude_none=True) + return {k: v for k, v in data.items() if v not in ([], {})} + + class StorageConfig(BaseModel): """Storage configuration for the runtime.""" @@ -232,9 +250,9 @@ class StorageConfig(BaseModel): default_factory=SfsTurboConfig, description="SFS Turbo storage configuration", ) - session_storage_mount_path: str | None = Field( - default=None, - description="Session storage mount path in the container", + session_storage: SessionStorageConfig | None = Field( + default_factory=SessionStorageConfig, + description="Session storage configuration", ) model_config = { @@ -254,8 +272,10 @@ def to_dict(self) -> dict[str, Any]: item = st.to_dict() if item: result["sfs_turbo"] = [item] - if self.session_storage_mount_path is not None: - result["session_storage_mount_path"] = self.session_storage_mount_path + if self.session_storage is not None: + ss = self.session_storage.to_dict() + if ss: + result["session_storage"] = ss return result diff --git a/tests/unit/toolkit/operations/runtime/test_config.py b/tests/unit/toolkit/operations/runtime/test_config.py index b2dc2a5a..fbe77da0 100644 --- a/tests/unit/toolkit/operations/runtime/test_config.py +++ b/tests/unit/toolkit/operations/runtime/test_config.py @@ -28,6 +28,7 @@ CustomJWTAuthConfig, InboundIdentityConfig, SfsTurboConfig, + SessionStorageConfig, StorageConfig, ) @@ -589,21 +590,21 @@ def test_storage_config_default_to_dict_is_empty(self): assert StorageConfig().to_dict() == {} def test_storage_config_to_dict_session_storage_only(self): - """StorageConfig with only session_storage_mount_path serializes correctly.""" + """StorageConfig with only session_storage serializes correctly.""" cfg = StorageConfig( - session_storage_mount_path="/home/user/sessions", + session_storage=SessionStorageConfig(mount_path="/home/user/sessions"), ) result = cfg.to_dict() - assert result == {"session_storage_mount_path": "/home/user/sessions"} + assert result == {"session_storage": {"mount_path": "/home/user/sessions"}} def test_storage_config_to_dict_both_fields(self): - """StorageConfig with both sfs_turbo and session_storage_mount_path.""" + """StorageConfig with both sfs_turbo and session_storage.""" cfg = StorageConfig( sfs_turbo=SfsTurboConfig( sfs_turbo_id="12345678-1234-1234-1234-123456789012", mount_path="/data", ), - session_storage_mount_path="/home/user/sessions", + session_storage=SessionStorageConfig(mount_path="/home/user/sessions"), ) result = cfg.to_dict() assert result == { @@ -611,7 +612,7 @@ def test_storage_config_to_dict_both_fields(self): "sfs_turbo_id": "12345678-1234-1234-1234-123456789012", "mount_path": "/data", }], - "session_storage_mount_path": "/home/user/sessions", + "session_storage": {"mount_path": "/home/user/sessions"}, } def test_invalid_sfs_turbo_id_rejected(self): diff --git a/tests/unit/toolkit/operations/runtime/test_deploy.py b/tests/unit/toolkit/operations/runtime/test_deploy.py index a88d0a89..3c7af6d6 100644 --- a/tests/unit/toolkit/operations/runtime/test_deploy.py +++ b/tests/unit/toolkit/operations/runtime/test_deploy.py @@ -10,6 +10,7 @@ from agentarts.toolkit.utils.runtime.config import ( AgentArtsConfig, AgentArtsRuntimeConfig, + SessionStorageConfig, SfsTurboConfig, StorageConfig, ) @@ -223,7 +224,7 @@ def test_storage_config_forwarded(self, mock_client, tmp_path, monkeypatch): @patch("agentarts.toolkit.operations.runtime.deploy.RuntimeClient") def test_storage_config_session_storage_only_forwarded(self, mock_client, tmp_path, monkeypatch): - """session_storage_mount_path alone is forwarded without sfs_turbo.""" + """session_storage alone is forwarded without sfs_turbo.""" monkeypatch.chdir(tmp_path) mock_client_instance = MagicMock() @@ -236,7 +237,7 @@ def test_storage_config_session_storage_only_forwarded(self, mock_client, tmp_pa agent_config = AgentArtsConfig( runtime=AgentArtsRuntimeConfig( storage_config=StorageConfig( - session_storage_mount_path="/home/user/sessions", + session_storage=SessionStorageConfig(mount_path="/home/user/sessions"), ), ), ) @@ -250,12 +251,12 @@ def test_storage_config_session_storage_only_forwarded(self, mock_client, tmp_pa call_args = mock_client_instance.create_or_update_agent.call_args assert call_args.kwargs["storage_config"] == { - "session_storage_mount_path": "/home/user/sessions", + "session_storage": {"mount_path": "/home/user/sessions"}, } @patch("agentarts.toolkit.operations.runtime.deploy.RuntimeClient") def test_storage_config_both_fields_forwarded(self, mock_client, tmp_path, monkeypatch): - """Both sfs_turbo and session_storage_mount_path are forwarded together.""" + """Both sfs_turbo and session_storage are forwarded together.""" monkeypatch.chdir(tmp_path) mock_client_instance = MagicMock() @@ -272,7 +273,7 @@ def test_storage_config_both_fields_forwarded(self, mock_client, tmp_path, monke sfs_turbo_id="12345678-1234-1234-1234-123456789012", mount_path="/data", ), - session_storage_mount_path="/home/user/sessions", + session_storage=SessionStorageConfig(mount_path="/home/user/sessions"), ), ), ) @@ -290,7 +291,7 @@ def test_storage_config_both_fields_forwarded(self, mock_client, tmp_path, monke "sfs_turbo_id": "12345678-1234-1234-1234-123456789012", "mount_path": "/data", }], - "session_storage_mount_path": "/home/user/sessions", + "session_storage": {"mount_path": "/home/user/sessions"}, } @patch("agentarts.toolkit.operations.runtime.deploy.RuntimeClient")