agentscope-runtime 1.0.0b2__py3-none-any.whl → 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. agentscope_runtime/adapters/agentscope/message.py +78 -10
  2. agentscope_runtime/adapters/agentscope/stream.py +155 -101
  3. agentscope_runtime/adapters/agentscope/tool/tool.py +1 -3
  4. agentscope_runtime/adapters/agno/__init__.py +0 -0
  5. agentscope_runtime/adapters/agno/message.py +30 -0
  6. agentscope_runtime/adapters/agno/stream.py +122 -0
  7. agentscope_runtime/adapters/langgraph/__init__.py +12 -0
  8. agentscope_runtime/adapters/langgraph/message.py +257 -0
  9. agentscope_runtime/adapters/langgraph/stream.py +205 -0
  10. agentscope_runtime/cli/__init__.py +7 -0
  11. agentscope_runtime/cli/cli.py +63 -0
  12. agentscope_runtime/cli/commands/__init__.py +2 -0
  13. agentscope_runtime/cli/commands/chat.py +815 -0
  14. agentscope_runtime/cli/commands/deploy.py +1062 -0
  15. agentscope_runtime/cli/commands/invoke.py +58 -0
  16. agentscope_runtime/cli/commands/list_cmd.py +103 -0
  17. agentscope_runtime/cli/commands/run.py +176 -0
  18. agentscope_runtime/cli/commands/sandbox.py +128 -0
  19. agentscope_runtime/cli/commands/status.py +60 -0
  20. agentscope_runtime/cli/commands/stop.py +185 -0
  21. agentscope_runtime/cli/commands/web.py +166 -0
  22. agentscope_runtime/cli/loaders/__init__.py +6 -0
  23. agentscope_runtime/cli/loaders/agent_loader.py +295 -0
  24. agentscope_runtime/cli/state/__init__.py +10 -0
  25. agentscope_runtime/cli/utils/__init__.py +18 -0
  26. agentscope_runtime/cli/utils/console.py +378 -0
  27. agentscope_runtime/cli/utils/validators.py +118 -0
  28. agentscope_runtime/engine/app/agent_app.py +15 -5
  29. agentscope_runtime/engine/deployers/__init__.py +1 -0
  30. agentscope_runtime/engine/deployers/agentrun_deployer.py +154 -24
  31. agentscope_runtime/engine/deployers/base.py +27 -2
  32. agentscope_runtime/engine/deployers/kubernetes_deployer.py +158 -31
  33. agentscope_runtime/engine/deployers/local_deployer.py +188 -25
  34. agentscope_runtime/engine/deployers/modelstudio_deployer.py +109 -18
  35. agentscope_runtime/engine/deployers/state/__init__.py +9 -0
  36. agentscope_runtime/engine/deployers/state/manager.py +388 -0
  37. agentscope_runtime/engine/deployers/state/schema.py +96 -0
  38. agentscope_runtime/engine/deployers/utils/build_cache.py +736 -0
  39. agentscope_runtime/engine/deployers/utils/detached_app.py +105 -30
  40. agentscope_runtime/engine/deployers/utils/docker_image_utils/docker_image_builder.py +31 -10
  41. agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +15 -8
  42. agentscope_runtime/engine/deployers/utils/docker_image_utils/image_factory.py +30 -2
  43. agentscope_runtime/engine/deployers/utils/k8s_utils.py +241 -0
  44. agentscope_runtime/engine/deployers/utils/package.py +56 -6
  45. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +68 -9
  46. agentscope_runtime/engine/deployers/utils/service_utils/process_manager.py +155 -5
  47. agentscope_runtime/engine/deployers/utils/wheel_packager.py +107 -123
  48. agentscope_runtime/engine/runner.py +32 -12
  49. agentscope_runtime/engine/schemas/agent_schemas.py +21 -7
  50. agentscope_runtime/engine/schemas/exception.py +580 -0
  51. agentscope_runtime/engine/services/agent_state/__init__.py +2 -0
  52. agentscope_runtime/engine/services/agent_state/state_service_factory.py +55 -0
  53. agentscope_runtime/engine/services/memory/__init__.py +2 -0
  54. agentscope_runtime/engine/services/memory/memory_service_factory.py +126 -0
  55. agentscope_runtime/engine/services/sandbox/__init__.py +2 -0
  56. agentscope_runtime/engine/services/sandbox/sandbox_service_factory.py +49 -0
  57. agentscope_runtime/engine/services/service_factory.py +119 -0
  58. agentscope_runtime/engine/services/session_history/__init__.py +2 -0
  59. agentscope_runtime/engine/services/session_history/session_history_service_factory.py +73 -0
  60. agentscope_runtime/engine/services/utils/tablestore_service_utils.py +35 -10
  61. agentscope_runtime/engine/tracing/wrapper.py +49 -31
  62. agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +113 -39
  63. agentscope_runtime/sandbox/box/shared/routers/mcp_utils.py +20 -4
  64. agentscope_runtime/sandbox/utils.py +2 -0
  65. agentscope_runtime/version.py +1 -1
  66. {agentscope_runtime-1.0.0b2.dist-info → agentscope_runtime-1.0.2.dist-info}/METADATA +82 -11
  67. {agentscope_runtime-1.0.0b2.dist-info → agentscope_runtime-1.0.2.dist-info}/RECORD +71 -36
  68. {agentscope_runtime-1.0.0b2.dist-info → agentscope_runtime-1.0.2.dist-info}/entry_points.txt +1 -0
  69. {agentscope_runtime-1.0.0b2.dist-info → agentscope_runtime-1.0.2.dist-info}/WHEEL +0 -0
  70. {agentscope_runtime-1.0.0b2.dist-info → agentscope_runtime-1.0.2.dist-info}/licenses/LICENSE +0 -0
  71. {agentscope_runtime-1.0.0b2.dist-info → agentscope_runtime-1.0.2.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,17 @@
1
1
  # -*- coding: utf-8 -*-
2
- # pylint:disable=protected-access, unused-argument
2
+ # pylint:disable=protected-access, unused-argument, too-many-branches
3
3
 
4
4
  import asyncio
5
5
  import logging
6
6
  import os
7
7
  import socket
8
8
  import threading
9
+ from datetime import datetime
9
10
  from typing import Callable, Optional, Type, Any, Dict, Union, List
10
11
 
11
12
  import uvicorn
12
13
 
14
+ from agentscope_runtime.engine.deployers.state import Deployment
13
15
  from .adapter.protocol_adapter import ProtocolAdapter
14
16
  from .base import DeployManager
15
17
  from .utils.deployment_modes import DeploymentMode
@@ -29,7 +31,7 @@ class LocalDeployManager(DeployManager):
29
31
  def __init__(
30
32
  self,
31
33
  host: str = "127.0.0.1",
32
- port: int = 8000,
34
+ port: int = 8090,
33
35
  shutdown_timeout: int = 30,
34
36
  startup_timeout: int = 30,
35
37
  logger: Optional[logging.Logger] = None,
@@ -80,6 +82,9 @@ class LocalDeployManager(DeployManager):
80
82
  broker_url: Optional[str] = None,
81
83
  backend_url: Optional[str] = None,
82
84
  enable_embedded_worker: bool = False,
85
+ # New parameters for project-based deployment
86
+ project_dir: Optional[str] = None,
87
+ entrypoint: Optional[str] = None,
83
88
  **kwargs: Any,
84
89
  ) -> Dict[str, str]:
85
90
  """Deploy using unified FastAPI architecture.
@@ -100,6 +105,8 @@ class LocalDeployManager(DeployManager):
100
105
  backend_url: Celery backend URL for result storage
101
106
  enable_embedded_worker: Whether to run Celery worker
102
107
  embedded in the app
108
+ project_dir: Project directory (for DETACHED_PROCESS mode)
109
+ entrypoint: Entrypoint specification (for DETACHED_PROCESS mode)
103
110
  **kwargs: Additional keyword arguments
104
111
 
105
112
  Returns:
@@ -152,6 +159,8 @@ class LocalDeployManager(DeployManager):
152
159
  after_finish=after_finish,
153
160
  custom_endpoints=custom_endpoints,
154
161
  protocol_adapters=protocol_adapters,
162
+ project_dir=project_dir,
163
+ entrypoint=entrypoint,
155
164
  **kwargs,
156
165
  )
157
166
  else:
@@ -171,6 +180,7 @@ class LocalDeployManager(DeployManager):
171
180
  broker_url: Optional[str] = None,
172
181
  backend_url: Optional[str] = None,
173
182
  enable_embedded_worker: bool = False,
183
+ agent_source: Optional[str] = None,
174
184
  **kwargs,
175
185
  ) -> Dict[str, str]:
176
186
  """Deploy in daemon thread mode."""
@@ -208,21 +218,43 @@ class LocalDeployManager(DeployManager):
208
218
  await self._wait_for_server_ready(self._startup_timeout)
209
219
 
210
220
  self.is_running = True
211
- self.deploy_id = f"daemon_{self.host}_{self.port}"
221
+
222
+ url = f"http://{self.host}:{self.port}"
212
223
 
213
224
  self._logger.info(
214
- f"FastAPI service started at http://{self.host}:{self.port}",
225
+ f"FastAPI service started at {url}",
226
+ )
227
+
228
+ deployment = Deployment(
229
+ id=self.deploy_id,
230
+ platform="local",
231
+ url=url,
232
+ status="running",
233
+ created_at=datetime.now().isoformat(),
234
+ agent_source=agent_source,
235
+ config={
236
+ "mode": DeploymentMode.DAEMON_THREAD,
237
+ "host": self.host,
238
+ "port": self.port,
239
+ "broker_url": broker_url,
240
+ "backend_url": backend_url,
241
+ "enable_embedded_worker": enable_embedded_worker,
242
+ },
215
243
  )
244
+ self.state_manager.save(deployment)
216
245
 
217
246
  return {
218
247
  "deploy_id": self.deploy_id,
219
- "url": f"http://{self.host}:{self.port}",
248
+ "url": url,
220
249
  }
221
250
 
222
251
  async def _deploy_detached_process(
223
252
  self,
224
253
  runner: Optional[Any] = None,
225
254
  protocol_adapters: Optional[list[ProtocolAdapter]] = None,
255
+ project_dir: Optional[str] = None,
256
+ entrypoint: Optional[str] = None,
257
+ agent_source: Optional[str] = None,
226
258
  **kwargs,
227
259
  ) -> Dict[str, str]:
228
260
  """Deploy in detached process mode."""
@@ -230,9 +262,14 @@ class LocalDeployManager(DeployManager):
230
262
  "Deploying FastAPI service in detached process mode...",
231
263
  )
232
264
 
233
- if runner is None and self._app is None:
265
+ # Clean up old log files (older than 24 hours)
266
+ ProcessManager.cleanup_old_logs(max_age_hours=24)
267
+
268
+ # Original behavior: require app or runner or entrypoint
269
+ if runner is None and self._app is None and entrypoint is None:
234
270
  raise ValueError(
235
- "Detached process mode requires an app or runner",
271
+ "Detached process mode requires an app, runner, "
272
+ "project_dir, or entrypoint",
236
273
  )
237
274
 
238
275
  if "agent" in kwargs:
@@ -245,18 +282,29 @@ class LocalDeployManager(DeployManager):
245
282
  app=self._app,
246
283
  runner=runner,
247
284
  protocol_adapters=protocol_adapters,
285
+ entrypoint=entrypoint,
248
286
  **kwargs,
249
287
  )
250
288
 
289
+ if not project_dir:
290
+ raise RuntimeError("Failed to parse project directory")
291
+
251
292
  try:
252
293
  entry_script = get_bundle_entry_script(project_dir)
253
294
  script_path = os.path.join(project_dir, entry_script)
254
-
295
+ env = kwargs.get("environment", {}) or {}
296
+ env.update(
297
+ {
298
+ "HOST": self.host,
299
+ "PORT": str(self.port),
300
+ },
301
+ )
255
302
  # Start detached process using the packaged project
256
303
  pid = await self.process_manager.start_detached_process(
257
304
  script_path=script_path,
258
305
  host=self.host,
259
306
  port=self.port,
307
+ env=env,
260
308
  )
261
309
 
262
310
  self._detached_process_pid = pid
@@ -273,35 +321,65 @@ class LocalDeployManager(DeployManager):
273
321
  )
274
322
 
275
323
  if not service_ready:
276
- raise RuntimeError("Service did not start within timeout")
324
+ # Check if process is still running
325
+ is_running = self.process_manager.is_process_running(pid)
326
+
327
+ # Get process logs
328
+ logs = self.process_manager.get_process_logs(max_lines=50)
329
+
330
+ # Log the detailed error for debugging
331
+ self._logger.error(
332
+ f"Service did not start within timeout. "
333
+ f"Process (PID: {pid}) status: "
334
+ f"{'running' if is_running else 'terminated'}. "
335
+ f"Host: {self.host}, Port: {self.port}.\n\n"
336
+ f"Process logs:\n{logs}",
337
+ )
338
+
339
+ # Raise a simple error message
340
+ raise RuntimeError(
341
+ "Service failed to start. Check logs above for details.",
342
+ )
277
343
 
278
344
  self.is_running = True
279
- self.deploy_id = f"detached_{pid}"
345
+
346
+ url = f"http://{self.host}:{self.port}"
280
347
 
281
348
  self._logger.info(
282
349
  f"FastAPI service started in detached process (PID: {pid})",
283
350
  )
284
351
 
352
+ deployment = Deployment(
353
+ id=self.deploy_id,
354
+ platform="local",
355
+ url=url,
356
+ status="running",
357
+ created_at=datetime.now().isoformat(),
358
+ agent_source=agent_source,
359
+ config={
360
+ "mode": DeploymentMode.DETACHED_PROCESS,
361
+ "host": self.host,
362
+ "port": self.port,
363
+ "pid": pid,
364
+ "pid_file": self._detached_pid_file,
365
+ "project_dir": project_dir,
366
+ },
367
+ )
368
+ self.state_manager.save(deployment)
369
+
285
370
  return {
286
371
  "deploy_id": self.deploy_id,
287
- "url": f"http://{self.host}:{self.port}",
372
+ "url": url,
288
373
  }
289
374
 
290
375
  except Exception as e:
291
- # Cleanup on failure
292
- if os.path.exists(project_dir):
293
- try:
294
- import shutil
295
-
296
- shutil.rmtree(project_dir)
297
- except OSError:
298
- pass
299
376
  raise e
300
377
 
301
378
  @staticmethod
302
379
  async def create_detached_project(
303
380
  app=None,
304
381
  runner: Optional[Any] = None,
382
+ entrypoint: Optional[str] = None,
305
383
  endpoint_path: str = "/process",
306
384
  requirements: Optional[Union[str, List[str]]] = None,
307
385
  extra_packages: Optional[List[str]] = None,
@@ -310,6 +388,7 @@ class LocalDeployManager(DeployManager):
310
388
  broker_url: Optional[str] = None,
311
389
  backend_url: Optional[str] = None,
312
390
  enable_embedded_worker: bool = False,
391
+ platform: str = "local",
313
392
  **kwargs,
314
393
  ) -> str:
315
394
  project_dir, _ = build_detached_app(
@@ -317,18 +396,81 @@ class LocalDeployManager(DeployManager):
317
396
  runner=runner,
318
397
  requirements=requirements,
319
398
  extra_packages=extra_packages,
399
+ platform=platform,
400
+ entrypoint=entrypoint,
320
401
  **kwargs,
321
402
  )
322
403
 
323
404
  return project_dir
324
405
 
325
- async def stop(self) -> None:
326
- """Stop the FastAPI service (unified method for all modes)."""
327
- if not self.is_running:
328
- self._logger.warning("Service is not running")
329
- return
406
+ async def stop(
407
+ self,
408
+ deploy_id: str,
409
+ **kwargs,
410
+ ) -> Dict[str, Any]:
411
+ """Stop the FastAPI service.
412
+
413
+ Args:
414
+ deploy_id: Deployment identifier
415
+ **kwargs: Additional parameters
330
416
 
417
+ Returns:
418
+ Dict with success status, message, and details
419
+ """
420
+ # If URL not provided, try to get it from state manager
331
421
  try:
422
+ deployment = self.state_manager.get(deploy_id)
423
+ if deployment:
424
+ url = deployment.url
425
+ self._logger.debug(f"Fetched URL from state: {url}")
426
+ except Exception as e:
427
+ self._logger.debug(f"Could not fetch URL from state: {e}")
428
+
429
+ if not deployment:
430
+ return {
431
+ "success": False,
432
+ "message": "Deploy id not found",
433
+ "details": {
434
+ "deploy_id": deploy_id,
435
+ "error": "Deploy id not found",
436
+ },
437
+ }
438
+
439
+ # Only attempt HTTP shutdown for detached process mode
440
+ # In daemon thread mode, HTTP shutdown would kill the entire process
441
+ # (including pytest), so we skip it and use direct stop methods instead
442
+ if (
443
+ url
444
+ and deployment.config["mode"] == DeploymentMode.DETACHED_PROCESS
445
+ ):
446
+ try:
447
+ import requests
448
+
449
+ response = requests.post(f"{url}/shutdown", timeout=5)
450
+ if response.status_code == 200:
451
+ # Remove from state manager on successful shutdown
452
+ try:
453
+ self.state_manager.update_status(deploy_id, "stopped")
454
+ except KeyError:
455
+ self._logger.debug(
456
+ f"Deployment {deploy_id} not found "
457
+ f"in state (already removed)",
458
+ )
459
+ self.is_running = False
460
+ return {
461
+ "success": True,
462
+ "message": "Shutdown signal sent to detached process",
463
+ "details": {"url": url, "deploy_id": deploy_id},
464
+ }
465
+
466
+ except requests.exceptions.RequestException as e:
467
+ # If HTTP shutdown fails, continue with direct stop methods
468
+ self._logger.debug(
469
+ f"HTTP shutdown failed, falling back to direct stop: {e}",
470
+ )
471
+
472
+ try:
473
+ # when run in from main process instead of cli, make sure close
332
474
  if self._detached_process_pid:
333
475
  # Detached process mode
334
476
  await self._stop_detached_process()
@@ -336,9 +478,27 @@ class LocalDeployManager(DeployManager):
336
478
  # Daemon thread mode
337
479
  await self._stop_daemon_thread()
338
480
 
481
+ # Remove from state manager on successful stop
482
+ try:
483
+ self.state_manager.update_status(deploy_id, "stopped")
484
+ except KeyError:
485
+ self._logger.debug(
486
+ f"Deployment {deploy_id} not found in state (already "
487
+ f"removed)",
488
+ )
489
+
490
+ return {
491
+ "success": True,
492
+ "message": "Service stopped successfully",
493
+ "details": {"deploy_id": deploy_id},
494
+ }
339
495
  except Exception as e:
340
496
  self._logger.error(f"Failed to stop service: {e}")
341
- raise RuntimeError(f"Failed to stop FastAPI service: {e}") from e
497
+ return {
498
+ "success": False,
499
+ "message": f"Failed to stop service: {e}",
500
+ "details": {"deploy_id": deploy_id, "error": str(e)},
501
+ }
342
502
 
343
503
  async def _stop_daemon_thread(self):
344
504
  """Stop daemon thread mode service."""
@@ -387,6 +547,9 @@ class LocalDeployManager(DeployManager):
387
547
  if self._detached_pid_file:
388
548
  self.process_manager.cleanup_pid_file(self._detached_pid_file)
389
549
 
550
+ # Cleanup log file (keep file for debugging)
551
+ self.process_manager.cleanup_log_file(keep_file=True)
552
+
390
553
  # Reset state
391
554
  self._detached_process_pid = None
392
555
  self._detached_pid_file = None
@@ -7,9 +7,9 @@
7
7
  import json
8
8
  import logging
9
9
  import os
10
- import time
10
+ from datetime import datetime
11
11
  from pathlib import Path
12
- from typing import Dict, Optional, List, Union, Tuple
12
+ from typing import Dict, Optional, List, Union, Tuple, Any
13
13
 
14
14
  import requests
15
15
  from pydantic import BaseModel, Field
@@ -17,11 +17,14 @@ from pydantic import BaseModel, Field
17
17
  from .adapter.protocol_adapter import ProtocolAdapter
18
18
  from .base import DeployManager
19
19
  from .local_deployer import LocalDeployManager
20
+ from .state import Deployment
20
21
  from .utils.detached_app import get_bundle_entry_script
22
+ from .utils.package import generate_build_directory
21
23
  from .utils.wheel_packager import (
22
24
  generate_wrapper_project,
23
25
  build_wheel,
24
26
  default_deploy_name,
27
+ get_user_bundle_appdir,
25
28
  )
26
29
 
27
30
  logger = logging.getLogger(__name__)
@@ -538,8 +541,9 @@ class ModelstudioDeployManager(DeployManager):
538
541
  oss_config: Optional[OSSConfig] = None,
539
542
  modelstudio_config: Optional[ModelstudioConfig] = None,
540
543
  build_root: Optional[Union[str, Path]] = None,
544
+ state_manager=None,
541
545
  ) -> None:
542
- super().__init__()
546
+ super().__init__(state_manager=state_manager)
543
547
  self.oss_config = oss_config or OSSConfig.from_env()
544
548
  self.modelstudio_config = (
545
549
  modelstudio_config or ModelstudioConfig.from_env()
@@ -552,11 +556,11 @@ class ModelstudioDeployManager(DeployManager):
552
556
  cmd: Optional[str] = None,
553
557
  deploy_name: Optional[str] = None,
554
558
  telemetry_enabled: bool = True,
559
+ environment: Optional[Dict[str, str]] = None,
560
+ requirements: Optional[Union[str, List[str]]] = None,
555
561
  ) -> Tuple[Path, str]:
556
562
  """
557
- 校验参数、生成 wrapper 项目并构建 wheel
558
-
559
- 返回: (wheel_path, wrapper_project_dir, name)
563
+ generate temp project path and build wheel.
560
564
  """
561
565
  if not project_dir or not cmd:
562
566
  raise ValueError(
@@ -569,18 +573,18 @@ class ModelstudioDeployManager(DeployManager):
569
573
  raise FileNotFoundError(f"Project dir not found: {project_dir}")
570
574
 
571
575
  name = deploy_name or default_deploy_name()
572
- proj_root = project_dir.resolve()
576
+
577
+ # Generate build directory with platform-aware naming
573
578
  if isinstance(self.build_root, Path):
574
579
  effective_build_root = self.build_root.resolve()
575
580
  else:
576
581
  if self.build_root:
577
582
  effective_build_root = Path(self.build_root).resolve()
578
583
  else:
579
- effective_build_root = (
580
- proj_root.parent / ".agentscope_runtime_builds"
581
- ).resolve()
584
+ # Use centralized directory generation function
585
+ effective_build_root = generate_build_directory("modelstudio")
582
586
 
583
- build_dir = effective_build_root / f"build-{int(time.time())}"
587
+ build_dir = effective_build_root
584
588
  build_dir.mkdir(parents=True, exist_ok=True)
585
589
 
586
590
  logger.info("Generating wrapper project for %s", name)
@@ -590,10 +594,15 @@ class ModelstudioDeployManager(DeployManager):
590
594
  start_cmd=cmd,
591
595
  deploy_name=name,
592
596
  telemetry_enabled=telemetry_enabled,
597
+ requirements=requirements,
593
598
  )
594
599
 
600
+ # pass environments to the project from user setting
601
+ user_bundle_app_dir = get_user_bundle_appdir(build_dir, project_dir)
602
+ self._generate_env_file(user_bundle_app_dir, environment)
595
603
  logger.info("Building wheel under %s", wrapper_project_dir)
596
604
  wheel_path = build_wheel(wrapper_project_dir)
605
+
597
606
  return wheel_path, name
598
607
 
599
608
  def _generate_env_file(
@@ -616,7 +625,9 @@ class ModelstudioDeployManager(DeployManager):
616
625
  variables provided
617
626
  """
618
627
  if not environment:
619
- return None
628
+ environment = {}
629
+ environment["HOST"] = os.environ.get("HOST", "0.0.0.0")
630
+ environment["PORT"] = int(os.environ.get("PORT", "8080"))
620
631
 
621
632
  project_path = Path(project_dir).resolve()
622
633
  if not project_path.exists():
@@ -731,14 +742,19 @@ class ModelstudioDeployManager(DeployManager):
731
742
  resource_name (deploy_name), and workspace_id.
732
743
  """
733
744
  if not agent_id:
734
- if not runner and not project_dir and not external_whl_path:
745
+ if (
746
+ not app
747
+ and not runner
748
+ and not project_dir
749
+ and not external_whl_path
750
+ ):
735
751
  raise ValueError(
736
- "Either runner, project_dir, "
752
+ "Either app, runner, project_dir, "
737
753
  "or external_whl_path must be provided.",
738
754
  )
739
755
 
740
756
  try:
741
- if runner:
757
+ if runner or app:
742
758
  if "agent" in kwargs:
743
759
  kwargs.pop("agent")
744
760
 
@@ -751,6 +767,7 @@ class ModelstudioDeployManager(DeployManager):
751
767
  requirements=requirements,
752
768
  extra_packages=extra_packages,
753
769
  port=8080,
770
+ platform="modelstudio",
754
771
  **kwargs,
755
772
  )
756
773
  if project_dir:
@@ -793,6 +810,8 @@ class ModelstudioDeployManager(DeployManager):
793
810
  cmd=cmd,
794
811
  deploy_name=deploy_name,
795
812
  telemetry_enabled=telemetry_enabled,
813
+ environment=environment,
814
+ requirements=requirements,
796
815
  )
797
816
 
798
817
  console_url = ""
@@ -814,16 +833,41 @@ class ModelstudioDeployManager(DeployManager):
814
833
  telemetry_enabled,
815
834
  )
816
835
 
836
+ # Use base class UUID deploy_id (already set in __init__)
837
+ deploy_id = self.deploy_id
838
+
839
+ # Save deployment to state manager
840
+ if deploy_identifier:
841
+ deployment = Deployment(
842
+ id=deploy_id,
843
+ platform="modelstudio",
844
+ url=console_url,
845
+ status="running",
846
+ created_at=datetime.now().isoformat(),
847
+ agent_source=kwargs.get("agent_source"),
848
+ config={
849
+ "modelstudio_deploy_id": deploy_identifier,
850
+ "resource_name": name,
851
+ "workspace_id": os.environ.get(
852
+ "MODELSTUDIO_WORKSPACE_ID",
853
+ "",
854
+ ).strip(),
855
+ "wheel_path": str(wheel_path),
856
+ },
857
+ )
858
+ self.state_manager.save(deployment)
859
+
817
860
  result: Dict[str, str] = {
818
861
  "wheel_path": str(wheel_path),
819
862
  "resource_name": name,
820
863
  "url": console_url,
864
+ "deploy_id": deploy_id,
821
865
  }
822
866
  env_ws = os.environ.get("MODELSTUDIO_WORKSPACE_ID")
823
867
  if env_ws and env_ws.strip():
824
868
  result["workspace_id"] = env_ws.strip()
825
869
  if deploy_identifier:
826
- result["deploy_id"] = deploy_identifier
870
+ result["modelstudio_deploy_id"] = deploy_identifier
827
871
 
828
872
  return result
829
873
  except Exception as e:
@@ -835,8 +879,55 @@ class ModelstudioDeployManager(DeployManager):
835
879
  )
836
880
  raise
837
881
 
838
- async def stop(self) -> None: # pragma: no cover - not supported yet
839
- pass
882
+ async def stop(self, deploy_id: str, **kwargs) -> Dict[str, Any]:
883
+ """Stop ModelStudio deployment.
884
+
885
+ Note: ModelStudio stop API not yet available.
886
+
887
+ Args:
888
+ deploy_id: Deployment identifier
889
+ **kwargs: Additional parameters
890
+
891
+ Returns:
892
+ Dict with success status and message
893
+ """
894
+ # Try to get deployment info from state for context
895
+ deployment_info = None
896
+ try:
897
+ deployment = self.state_manager.get(deploy_id)
898
+ if deployment:
899
+ deployment_info = {
900
+ "url": deployment.url
901
+ if hasattr(deployment, "url")
902
+ else None,
903
+ "workspace_id": getattr(deployment, "workspace_id", None),
904
+ }
905
+ logger.debug(
906
+ f"Fetched deployment info from state: {deployment_info}",
907
+ )
908
+ except Exception as e:
909
+ logger.debug(f"Could not fetch deployment info from state: {e}")
910
+
911
+ # TODO: Implement when ModelStudio provides stop/delete API
912
+ # When API is available, call it here and then remove from state:
913
+ # self.state_manager.remove(deploy_id)
914
+
915
+ logger.warning(
916
+ f"ModelStudio stop not implemented for deploy_id={deploy_id} - API not yet available",
917
+ )
918
+
919
+ details = {
920
+ "deploy_id": deploy_id,
921
+ "note": "Manual cleanup required via ModelStudio console",
922
+ }
923
+ if deployment_info:
924
+ details.update(deployment_info)
925
+
926
+ return {
927
+ "success": False,
928
+ "message": "ModelStudio stop not implemented - API not yet available",
929
+ "details": details,
930
+ }
840
931
 
841
932
  def get_status(self) -> str: # pragma: no cover - not supported yet
842
933
  return "unknown"
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Deployment state management."""
3
+
4
+ from agentscope_runtime.engine.deployers.state.manager import (
5
+ DeploymentStateManager,
6
+ )
7
+ from agentscope_runtime.engine.deployers.state.schema import Deployment
8
+
9
+ __all__ = ["DeploymentStateManager", "Deployment"]