agentscope-runtime 1.0.2__py3-none-any.whl → 1.0.4__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 (58) hide show
  1. agentscope_runtime/adapters/agentscope/stream.py +2 -9
  2. agentscope_runtime/adapters/ms_agent_framework/__init__.py +0 -0
  3. agentscope_runtime/adapters/ms_agent_framework/message.py +205 -0
  4. agentscope_runtime/adapters/ms_agent_framework/stream.py +418 -0
  5. agentscope_runtime/adapters/utils.py +6 -0
  6. agentscope_runtime/cli/commands/deploy.py +383 -0
  7. agentscope_runtime/common/collections/redis_mapping.py +4 -1
  8. agentscope_runtime/common/container_clients/knative_client.py +466 -0
  9. agentscope_runtime/engine/__init__.py +4 -0
  10. agentscope_runtime/engine/app/agent_app.py +48 -5
  11. agentscope_runtime/engine/constant.py +1 -0
  12. agentscope_runtime/engine/deployers/__init__.py +12 -0
  13. agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +31 -1
  14. agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +458 -41
  15. agentscope_runtime/engine/deployers/adapter/a2a/a2a_registry.py +76 -0
  16. agentscope_runtime/engine/deployers/adapter/a2a/nacos_a2a_registry.py +749 -0
  17. agentscope_runtime/engine/deployers/agentrun_deployer.py +2 -2
  18. agentscope_runtime/engine/deployers/fc_deployer.py +1506 -0
  19. agentscope_runtime/engine/deployers/knative_deployer.py +290 -0
  20. agentscope_runtime/engine/deployers/kubernetes_deployer.py +3 -0
  21. agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +8 -2
  22. agentscope_runtime/engine/deployers/utils/docker_image_utils/image_factory.py +5 -0
  23. agentscope_runtime/engine/deployers/utils/net_utils.py +65 -0
  24. agentscope_runtime/engine/runner.py +17 -3
  25. agentscope_runtime/engine/schemas/exception.py +24 -0
  26. agentscope_runtime/engine/services/agent_state/redis_state_service.py +61 -8
  27. agentscope_runtime/engine/services/agent_state/state_service_factory.py +2 -5
  28. agentscope_runtime/engine/services/memory/redis_memory_service.py +129 -25
  29. agentscope_runtime/engine/services/session_history/redis_session_history_service.py +160 -34
  30. agentscope_runtime/engine/tracing/wrapper.py +18 -4
  31. agentscope_runtime/sandbox/__init__.py +14 -6
  32. agentscope_runtime/sandbox/box/base/__init__.py +2 -2
  33. agentscope_runtime/sandbox/box/base/base_sandbox.py +51 -1
  34. agentscope_runtime/sandbox/box/browser/__init__.py +2 -2
  35. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +198 -2
  36. agentscope_runtime/sandbox/box/filesystem/__init__.py +2 -2
  37. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +99 -2
  38. agentscope_runtime/sandbox/box/gui/__init__.py +2 -2
  39. agentscope_runtime/sandbox/box/gui/gui_sandbox.py +117 -1
  40. agentscope_runtime/sandbox/box/mobile/__init__.py +2 -2
  41. agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +247 -100
  42. agentscope_runtime/sandbox/box/sandbox.py +98 -65
  43. agentscope_runtime/sandbox/box/shared/routers/generic.py +36 -29
  44. agentscope_runtime/sandbox/build.py +50 -57
  45. agentscope_runtime/sandbox/client/__init__.py +6 -1
  46. agentscope_runtime/sandbox/client/async_http_client.py +339 -0
  47. agentscope_runtime/sandbox/client/base.py +74 -0
  48. agentscope_runtime/sandbox/client/http_client.py +108 -329
  49. agentscope_runtime/sandbox/enums.py +7 -0
  50. agentscope_runtime/sandbox/manager/sandbox_manager.py +264 -4
  51. agentscope_runtime/sandbox/manager/server/app.py +7 -1
  52. agentscope_runtime/version.py +1 -1
  53. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/METADATA +109 -29
  54. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/RECORD +58 -46
  55. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/WHEEL +0 -0
  56. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/entry_points.txt +0 -0
  57. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/licenses/LICENSE +0 -0
  58. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,749 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Nacos Registry Implementation for A2A Protocol
4
+
5
+ This module provides a Nacos-based registry implementation for A2A protocol
6
+ agent service registration. It serves as the default registry implementation
7
+ for A2A protocol adapters.
8
+ """
9
+ import asyncio
10
+ import logging
11
+ import os
12
+ import threading
13
+ from enum import Enum
14
+ from typing import Any, Optional, TYPE_CHECKING, List
15
+
16
+ from a2a.types import AgentCard
17
+ from dotenv import find_dotenv, load_dotenv
18
+ from pydantic import ConfigDict
19
+ from pydantic_settings import BaseSettings
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Make the v2.nacos imports optional to avoid hard dependency at
24
+ # module import time.
25
+ if TYPE_CHECKING:
26
+ from v2.nacos import ClientConfig, ClientConfigBuilder
27
+ from v2.nacos.ai.model.ai_param import (
28
+ RegisterAgentEndpointParam,
29
+ ReleaseAgentCardParam,
30
+ )
31
+ from v2.nacos.ai.nacos_ai_service import NacosAIService
32
+
33
+ _NACOS_SDK_AVAILABLE = True
34
+ else:
35
+ try:
36
+ from v2.nacos import ClientConfig, ClientConfigBuilder
37
+ from v2.nacos.ai.model.ai_param import (
38
+ RegisterAgentEndpointParam,
39
+ ReleaseAgentCardParam,
40
+ )
41
+ from v2.nacos.ai.nacos_ai_service import NacosAIService
42
+
43
+ _NACOS_SDK_AVAILABLE = True
44
+ except Exception:
45
+ logger.warning(
46
+ "[NacosRegistry] Nacos SDK (nacos-sdk-python) is not available. "
47
+ "Install it with: pip install nacos-sdk-python",
48
+ )
49
+
50
+ class ClientConfig:
51
+ pass
52
+
53
+ class ClientConfigBuilder:
54
+ pass
55
+
56
+ class RegisterAgentEndpointParam:
57
+ pass
58
+
59
+ class ReleaseAgentCardParam:
60
+ pass
61
+
62
+ class NacosAIService:
63
+ pass
64
+
65
+ _NACOS_SDK_AVAILABLE = False
66
+
67
+ # Import after conditional imports to avoid circular dependencies
68
+ # flake8: noqa: E402
69
+ from .a2a_registry import ( # pylint: disable=wrong-import-position
70
+ A2ARegistry,
71
+ A2ATransportsProperties,
72
+ )
73
+
74
+
75
+ class NacosSettings(BaseSettings):
76
+ """Nacos-specific settings loaded from environment variables."""
77
+
78
+ NACOS_SERVER_ADDR: str = "localhost:8848"
79
+ NACOS_USERNAME: Optional[str] = None
80
+ NACOS_PASSWORD: Optional[str] = None
81
+ NACOS_NAMESPACE_ID: Optional[str] = None
82
+ NACOS_ACCESS_KEY: Optional[str] = None
83
+ NACOS_SECRET_KEY: Optional[str] = None
84
+
85
+ model_config = ConfigDict(
86
+ extra="allow",
87
+ )
88
+
89
+
90
+ _nacos_settings: Optional[NacosSettings] = None
91
+
92
+
93
+ def get_nacos_settings() -> NacosSettings:
94
+ """Return a singleton Nacos settings instance, loading .env files
95
+ if needed."""
96
+ global _nacos_settings
97
+
98
+ if _nacos_settings is None:
99
+ dotenv_path = find_dotenv(raise_error_if_not_found=False)
100
+ if dotenv_path:
101
+ load_dotenv(dotenv_path, override=False)
102
+ else:
103
+ if os.path.exists(".env.example"):
104
+ load_dotenv(".env.example", override=False)
105
+ _nacos_settings = NacosSettings()
106
+
107
+ return _nacos_settings
108
+
109
+
110
+ def _build_nacos_client_config(settings: NacosSettings) -> Any:
111
+ """Build Nacos client configuration from settings.
112
+
113
+ Supports both username/password and access key authentication.
114
+ """
115
+ try:
116
+ from v2.nacos import ClientConfigBuilder
117
+ except (ImportError, ModuleNotFoundError) as e:
118
+ logger.warning(
119
+ "[A2A] Nacos SDK (nacos-sdk-python) is not available. "
120
+ "Install it with: pip install nacos-sdk-python",
121
+ )
122
+ raise ImportError(
123
+ "Nacos SDK (nacos-sdk-python) is not available. "
124
+ "Install it with: pip install nacos-sdk-python",
125
+ ) from e
126
+
127
+ builder = ClientConfigBuilder().server_address(settings.NACOS_SERVER_ADDR)
128
+
129
+ if settings.NACOS_NAMESPACE_ID:
130
+ builder.namespace_id(settings.NACOS_NAMESPACE_ID)
131
+ logger.debug(
132
+ "[A2A] Using Nacos namespace: %s",
133
+ settings.NACOS_NAMESPACE_ID,
134
+ )
135
+
136
+ if settings.NACOS_USERNAME and settings.NACOS_PASSWORD:
137
+ builder.username(settings.NACOS_USERNAME).password(
138
+ settings.NACOS_PASSWORD,
139
+ )
140
+ logger.debug("[A2A] Using Nacos username/password authentication")
141
+
142
+ if settings.NACOS_ACCESS_KEY and settings.NACOS_SECRET_KEY:
143
+ builder.access_key(settings.NACOS_ACCESS_KEY).secret_key(
144
+ settings.NACOS_SECRET_KEY,
145
+ )
146
+ logger.debug("[A2A] Using Nacos access key authentication")
147
+
148
+ return builder.build()
149
+
150
+
151
+ def create_nacos_registry_from_env() -> Optional[A2ARegistry]:
152
+ """Create a NacosRegistry instance from environment settings.
153
+
154
+ Returns None if the required nacos SDK is not available or
155
+ construction fails.
156
+ """
157
+ if not _NACOS_SDK_AVAILABLE:
158
+ return None
159
+
160
+ try:
161
+ nacos_settings = get_nacos_settings()
162
+ nacos_client_config = _build_nacos_client_config(nacos_settings)
163
+ registry = NacosRegistry(nacos_client_config=nacos_client_config)
164
+
165
+ auth_methods = []
166
+ if nacos_settings.NACOS_USERNAME and nacos_settings.NACOS_PASSWORD:
167
+ auth_methods.append("username/password")
168
+ if nacos_settings.NACOS_ACCESS_KEY and nacos_settings.NACOS_SECRET_KEY:
169
+ auth_methods.append("access_key")
170
+ auth_status = ", ".join(auth_methods) if auth_methods else "disabled"
171
+
172
+ namespace_info = (
173
+ f", namespace={nacos_settings.NACOS_NAMESPACE_ID}"
174
+ if nacos_settings.NACOS_NAMESPACE_ID
175
+ else ""
176
+ )
177
+ logger.info(
178
+ f"[A2A] Created Nacos registry from environment: "
179
+ f"server={nacos_settings.NACOS_SERVER_ADDR}, "
180
+ f"authentication={auth_status}{namespace_info}",
181
+ )
182
+ return registry
183
+ except (ImportError, ModuleNotFoundError):
184
+ return None
185
+ except Exception:
186
+ logger.warning(
187
+ "[A2A] Failed to construct Nacos registry from settings",
188
+ exc_info=True,
189
+ )
190
+ return None
191
+
192
+
193
+ class RegistrationStatus(Enum):
194
+ """Registration task status."""
195
+
196
+ PENDING = "pending"
197
+ IN_PROGRESS = "in_progress"
198
+ COMPLETED = "completed"
199
+ FAILED = "failed"
200
+ CANCELLED = "cancelled"
201
+
202
+
203
+ class NacosRegistry(A2ARegistry):
204
+ """Nacos-based registry implementation for A2A protocol.
205
+
206
+ This registry registers A2A agent services to Nacos service
207
+ discovery system. It performs two-step registration:
208
+ 1. Publishes agent card to Nacos (agent metadata)
209
+ 2. Registers agent endpoint with host/port information (service location)
210
+
211
+ Args:
212
+ nacos_client_config: Optional Nacos client configuration.
213
+ If None, creates default config from environment variables.
214
+ """
215
+
216
+ def __init__(
217
+ self,
218
+ nacos_client_config: Optional[ClientConfig] = None,
219
+ ):
220
+ self._nacos_client_config = nacos_client_config
221
+ self._nacos_ai_service: Optional[NacosAIService] = None
222
+ self._register_task: Optional[asyncio.Task] = None
223
+ self._register_thread: Optional[threading.Thread] = None
224
+ self._registration_status: RegistrationStatus = (
225
+ RegistrationStatus.PENDING
226
+ )
227
+ self._registration_lock = threading.Lock()
228
+ self._shutdown_event = threading.Event()
229
+
230
+ def registry_name(self) -> str:
231
+ """Get the name of this registry implementation.
232
+
233
+ Returns:
234
+ Registry name: "nacos"
235
+ """
236
+ return "nacos"
237
+
238
+ def register(
239
+ self,
240
+ agent_card: AgentCard,
241
+ a2a_transports_properties: Optional[
242
+ List[A2ATransportsProperties]
243
+ ] = None,
244
+ ) -> None:
245
+ """Register an A2A agent service to Nacos.
246
+
247
+ This method initiates registration asynchronously in the
248
+ background. Exceptions during registration are caught and
249
+ logged, but not raised. Use `get_registration_status()` or
250
+ `wait_for_registration()` to check the result.
251
+
252
+ Args:
253
+ agent_card: The complete A2A agent card generated by runtime
254
+ a2a_transports_properties: List of transport configurations.
255
+ Each transport will be registered separately.
256
+ """
257
+ if not _NACOS_SDK_AVAILABLE:
258
+ logger.warning(
259
+ "[NacosRegistry] Nacos SDK (nacos-sdk-python) is not "
260
+ "available. Install it with: pip install nacos-sdk-python",
261
+ )
262
+ return
263
+
264
+ self._start_register_task(
265
+ agent_card=agent_card,
266
+ a2a_transports_properties=a2a_transports_properties,
267
+ )
268
+
269
+ def _start_register_task(
270
+ self,
271
+ agent_card: AgentCard,
272
+ a2a_transports_properties: Optional[
273
+ List[A2ATransportsProperties]
274
+ ] = None,
275
+ ) -> None:
276
+ """Start background Nacos registration task.
277
+
278
+ If there is an active asyncio event loop, schedule the
279
+ registration as a task on that loop. Otherwise, spawn a daemon
280
+ thread and run the registration using asyncio.run so
281
+ registration still occurs in synchronous contexts.
282
+ """
283
+ with self._registration_lock:
284
+ if self._shutdown_event.is_set():
285
+ logger.info(
286
+ "[NacosRegistry] Shutdown already requested, "
287
+ "skipping registration",
288
+ )
289
+ self._registration_status = RegistrationStatus.CANCELLED
290
+ return
291
+
292
+ if self._registration_status in (
293
+ RegistrationStatus.IN_PROGRESS,
294
+ RegistrationStatus.COMPLETED,
295
+ ):
296
+ logger.warning(
297
+ "[NacosRegistry] Registration already in "
298
+ "progress or completed, skipping",
299
+ )
300
+ return
301
+ self._registration_status = RegistrationStatus.PENDING
302
+
303
+ try:
304
+ loop = None
305
+ try:
306
+ loop = asyncio.get_running_loop()
307
+ except RuntimeError:
308
+ loop = None
309
+
310
+ if loop is not None:
311
+ with self._registration_lock:
312
+ self._registration_status = RegistrationStatus.IN_PROGRESS
313
+ self._register_task = loop.create_task(
314
+ self._register_to_nacos(
315
+ agent_card=agent_card,
316
+ a2a_transports_properties=a2a_transports_properties,
317
+ ),
318
+ )
319
+ logger.info(
320
+ "[NacosRegistry] Registration task scheduled on "
321
+ "running event loop",
322
+ )
323
+ return
324
+
325
+ def _thread_runner():
326
+ try:
327
+ with self._registration_lock:
328
+ if self._shutdown_event.is_set():
329
+ logger.info(
330
+ "[NacosRegistry] Shutdown requested "
331
+ "before registration started",
332
+ )
333
+ self._registration_status = (
334
+ RegistrationStatus.CANCELLED
335
+ )
336
+ return
337
+ self._registration_status = (
338
+ RegistrationStatus.IN_PROGRESS
339
+ )
340
+
341
+ asyncio.run(
342
+ self._register_to_nacos(
343
+ agent_card=agent_card,
344
+ a2a_transports_properties=(
345
+ a2a_transports_properties
346
+ ),
347
+ ),
348
+ )
349
+ with self._registration_lock:
350
+ if (
351
+ self._registration_status
352
+ == RegistrationStatus.IN_PROGRESS
353
+ ):
354
+ self._registration_status = (
355
+ RegistrationStatus.COMPLETED
356
+ )
357
+ except asyncio.CancelledError:
358
+ with self._registration_lock:
359
+ self._registration_status = (
360
+ RegistrationStatus.CANCELLED
361
+ )
362
+ logger.info("[NacosRegistry] Registration cancelled")
363
+ except Exception:
364
+ with self._registration_lock:
365
+ self._registration_status = RegistrationStatus.FAILED
366
+ logger.error(
367
+ "[NacosRegistry] Registration failed in "
368
+ "background thread",
369
+ exc_info=True,
370
+ )
371
+
372
+ thread = threading.Thread(
373
+ target=_thread_runner,
374
+ name="nacos-registry-register",
375
+ daemon=True,
376
+ )
377
+ thread.start()
378
+ with self._registration_lock:
379
+ self._register_thread = thread
380
+ logger.info(
381
+ "[NacosRegistry] Registration task started in "
382
+ "background thread",
383
+ )
384
+ except Exception:
385
+ with self._registration_lock:
386
+ self._registration_status = RegistrationStatus.FAILED
387
+ logger.warning(
388
+ "[NacosRegistry] Error starting registration task",
389
+ exc_info=True,
390
+ )
391
+
392
+ def _get_client_config(self) -> ClientConfig:
393
+ """Get Nacos client configuration.
394
+
395
+ Returns:
396
+ ClientConfig: Nacos client configuration
397
+ """
398
+ if self._nacos_client_config is not None:
399
+ return self._nacos_client_config
400
+
401
+ settings = get_nacos_settings()
402
+ return _build_nacos_client_config(settings)
403
+
404
+ # pylint: disable=too-many-branches,too-many-statements
405
+ async def _register_to_nacos(
406
+ self,
407
+ agent_card: AgentCard,
408
+ a2a_transports_properties: Optional[
409
+ List[A2ATransportsProperties]
410
+ ] = None,
411
+ ) -> None:
412
+ """Register agent to Nacos.
413
+
414
+ Performs two-step registration:
415
+ 1. Release agent card to Nacos
416
+ 2. Register agent endpoint with host/port information
417
+
418
+ On successful registration, the NacosAIService connection is kept
419
+ alive to maintain heartbeat and health checks. The service will be
420
+ closed in cleanup() during application shutdown.
421
+
422
+ Most exceptions are caught and logged without re-raising to prevent
423
+ background tasks from crashing the host. Only `asyncio.CancelledError`
424
+ is re-raised.
425
+
426
+ Args:
427
+ agent_card: The A2A agent card to register
428
+ a2a_transports_properties: Optional list of transport
429
+ configurations
430
+
431
+ Raises:
432
+ asyncio.CancelledError: If the registration task is cancelled
433
+ """
434
+ # Check if shutdown was requested
435
+ if self._shutdown_event.is_set():
436
+ with self._registration_lock:
437
+ self._registration_status = RegistrationStatus.CANCELLED
438
+ logger.info(
439
+ "[NacosRegistry] Registration cancelled due to shutdown",
440
+ )
441
+ return
442
+
443
+ client_config = self._get_client_config()
444
+ try:
445
+ self._nacos_ai_service = await NacosAIService.create_ai_service(
446
+ client_config,
447
+ )
448
+
449
+ # Check again after service creation
450
+ if self._shutdown_event.is_set():
451
+ with self._registration_lock:
452
+ self._registration_status = RegistrationStatus.CANCELLED
453
+ logger.info(
454
+ "[NacosRegistry] Registration cancelled after "
455
+ "service creation",
456
+ )
457
+ return
458
+
459
+ # Publish agent card
460
+ await self._nacos_ai_service.release_agent_card(
461
+ ReleaseAgentCardParam(agent_card=agent_card),
462
+ )
463
+ logger.info(
464
+ "[NacosRegistry] Agent card published: agent=%s",
465
+ agent_card.name,
466
+ )
467
+
468
+ # Check again before endpoint registration
469
+ if self._shutdown_event.is_set():
470
+ with self._registration_lock:
471
+ self._registration_status = RegistrationStatus.CANCELLED
472
+ logger.warning(
473
+ "[NacosRegistry] Registration cancelled after "
474
+ "card publish, endpoint may be partially "
475
+ "registered",
476
+ )
477
+ return
478
+
479
+ # Register agent endpoint
480
+ if a2a_transports_properties:
481
+ for transport in a2a_transports_properties:
482
+ host = transport.host
483
+ port = transport.port
484
+ endpoint_param = RegisterAgentEndpointParam(
485
+ agent_name=agent_card.name,
486
+ version=agent_card.version,
487
+ address=host,
488
+ port=port,
489
+ path=transport.path,
490
+ support_tls=transport.support_tls,
491
+ transport=transport.transport_type,
492
+ )
493
+
494
+ await self._nacos_ai_service.register_agent_endpoint(
495
+ endpoint_param,
496
+ )
497
+ logger.info(
498
+ "[NacosRegistry] Agent endpoint registered: "
499
+ "agent=%s, address=%s:%s, path=%s",
500
+ agent_card.name,
501
+ host,
502
+ port,
503
+ transport.path,
504
+ )
505
+
506
+ with self._registration_lock:
507
+ if self._registration_status == RegistrationStatus.IN_PROGRESS:
508
+ self._registration_status = RegistrationStatus.COMPLETED
509
+ # Service remains alive to maintain heartbeat and health checks.
510
+ # It will be closed in cleanup() during application shutdown.
511
+
512
+ except asyncio.CancelledError:
513
+ with self._registration_lock:
514
+ self._registration_status = RegistrationStatus.CANCELLED
515
+ logger.info("[NacosRegistry] Registration task cancelled")
516
+ try:
517
+ svc = self._nacos_ai_service
518
+ if svc is not None:
519
+ await svc.shutdown()
520
+ logger.debug(
521
+ "[NacosRegistry] NacosAIService closed due to "
522
+ "cancellation",
523
+ )
524
+ except Exception:
525
+ logger.debug(
526
+ "[NacosRegistry] Error closing NacosAIService on "
527
+ "cancellation",
528
+ exc_info=True,
529
+ )
530
+ raise
531
+ except Exception as e:
532
+ with self._registration_lock:
533
+ self._registration_status = RegistrationStatus.FAILED
534
+ # Log errors but don't re-raise; background tasks should
535
+ # not crash the host
536
+ logger.error(
537
+ "[NacosRegistry] Failed to register agent=%s: %s",
538
+ agent_card.name,
539
+ str(e),
540
+ exc_info=True,
541
+ )
542
+ try:
543
+ svc = self._nacos_ai_service
544
+ if svc is not None:
545
+ await svc.shutdown()
546
+ logger.debug(
547
+ "[NacosRegistry] NacosAIService closed due to "
548
+ "registration failure",
549
+ )
550
+ except Exception:
551
+ logger.debug(
552
+ "[NacosRegistry] Error closing NacosAIService on failure",
553
+ exc_info=True,
554
+ )
555
+
556
+ def get_registration_status(self) -> RegistrationStatus:
557
+ """Get the current registration status.
558
+
559
+ Returns:
560
+ RegistrationStatus: Current status of the registration task
561
+ """
562
+ with self._registration_lock:
563
+ return self._registration_status
564
+
565
+ async def wait_for_registration(
566
+ self,
567
+ timeout: Optional[float] = None,
568
+ ) -> RegistrationStatus:
569
+ """Wait for registration to complete, fail, or be cancelled.
570
+
571
+ Args:
572
+ timeout: Optional timeout in seconds. If None, waits indefinitely.
573
+
574
+ Returns:
575
+ RegistrationStatus: Final status of the registration
576
+ """
577
+ if self._register_task is not None:
578
+ # Task-based registration: wait for the task
579
+ try:
580
+ if timeout is not None:
581
+ await asyncio.wait_for(
582
+ self._register_task,
583
+ timeout=timeout,
584
+ )
585
+ else:
586
+ await self._register_task
587
+ except asyncio.TimeoutError:
588
+ logger.warning(
589
+ "[NacosRegistry] Wait for registration timed "
590
+ "out after %s seconds",
591
+ timeout,
592
+ )
593
+ except (asyncio.CancelledError, Exception):
594
+ pass # Task may have been cancelled or failed
595
+ elif self._register_thread is not None:
596
+ # Thread-based registration: wait for the thread (matching
597
+ # LocalDeployManager pattern)
598
+ self._register_thread.join(timeout=timeout)
599
+ if self._register_thread.is_alive():
600
+ logger.warning(
601
+ "[NacosRegistry] Registration thread did not "
602
+ "complete within timeout",
603
+ )
604
+
605
+ return self.get_registration_status()
606
+
607
+ # pylint: disable=too-many-branches,too-many-statements
608
+ async def cleanup(
609
+ self,
610
+ wait_for_completion: bool = True,
611
+ timeout: Optional[float] = 5.0,
612
+ ) -> None:
613
+ """Clean up registration resources and cancel ongoing
614
+ registration if needed.
615
+
616
+ This method should be called during shutdown to ensure proper cleanup.
617
+ It will:
618
+ 1. Signal shutdown to prevent new registration attempts
619
+ 2. Optionally wait for the registration to complete or timeout
620
+ 3. Cancel the asyncio task if running
621
+ 4. Wait for background thread if running
622
+ 5. Clean up service connections
623
+
624
+ Args:
625
+ wait_for_completion: If True, wait for registration to complete
626
+ before cancelling. If False, cancel immediately.
627
+ timeout: Maximum time to wait for completion (if
628
+ wait_for_completion=True). Default is 5 seconds.
629
+ """
630
+ logger.info("[NacosRegistry] Starting cleanup")
631
+
632
+ # Signal shutdown first to prevent new registration attempts
633
+ self._shutdown_event.set()
634
+
635
+ with self._registration_lock:
636
+ current_status = self._registration_status
637
+
638
+ # If registration is in progress or pending, handle cancellation
639
+ if current_status in (
640
+ RegistrationStatus.IN_PROGRESS,
641
+ RegistrationStatus.PENDING,
642
+ ):
643
+ if (
644
+ wait_for_completion
645
+ and current_status == RegistrationStatus.IN_PROGRESS
646
+ ):
647
+ logger.info(
648
+ "[NacosRegistry] Waiting for registration to "
649
+ "complete (timeout=%s)",
650
+ timeout,
651
+ )
652
+ try:
653
+ await self.wait_for_registration(timeout=timeout)
654
+ current_status = self.get_registration_status()
655
+ if current_status == RegistrationStatus.COMPLETED:
656
+ logger.info(
657
+ "[NacosRegistry] Registration completed "
658
+ "before shutdown",
659
+ )
660
+ elif current_status == RegistrationStatus.FAILED:
661
+ logger.warning(
662
+ "[NacosRegistry] Registration failed "
663
+ "before shutdown",
664
+ )
665
+ elif current_status == RegistrationStatus.CANCELLED:
666
+ logger.info(
667
+ "[NacosRegistry] Registration was cancelled",
668
+ )
669
+ except Exception as e:
670
+ logger.warning(
671
+ "[NacosRegistry] Error waiting for registration: %s",
672
+ e,
673
+ exc_info=True,
674
+ )
675
+ else:
676
+ if current_status == RegistrationStatus.PENDING:
677
+ logger.info(
678
+ "[NacosRegistry] Registration was pending, "
679
+ "marking as cancelled",
680
+ )
681
+ with self._registration_lock:
682
+ self._registration_status = (
683
+ RegistrationStatus.CANCELLED
684
+ )
685
+ else:
686
+ logger.info(
687
+ "[NacosRegistry] Skipping wait, cancelling "
688
+ "immediately",
689
+ )
690
+
691
+ # Cancel the task if it's still running (matching
692
+ # env_service pattern)
693
+ if (
694
+ self._register_task is not None
695
+ and not self._register_task.done()
696
+ ):
697
+ logger.info("[NacosRegistry] Cancelling registration task")
698
+ self._register_task.cancel()
699
+ try:
700
+ await self._register_task
701
+ except asyncio.CancelledError:
702
+ # Expected when task is cancelled: await on a
703
+ # cancelled task raises CancelledError
704
+ pass
705
+ except Exception as e:
706
+ logger.debug(
707
+ "[NacosRegistry] Error cancelling task: %s",
708
+ e,
709
+ exc_info=True,
710
+ )
711
+
712
+ # Wait for thread if it's still running (matching
713
+ # LocalDeployManager pattern)
714
+ if (
715
+ self._register_thread is not None
716
+ and self._register_thread.is_alive()
717
+ ):
718
+ logger.info(
719
+ "[NacosRegistry] Waiting for registration thread "
720
+ "to finish",
721
+ )
722
+ thread_timeout = (
723
+ timeout
724
+ if (
725
+ wait_for_completion
726
+ and current_status == RegistrationStatus.IN_PROGRESS
727
+ )
728
+ else 1.0
729
+ )
730
+ self._register_thread.join(timeout=thread_timeout)
731
+ if self._register_thread.is_alive():
732
+ logger.warning(
733
+ "[NacosRegistry] Registration thread did not "
734
+ "terminate, potential resource leak",
735
+ )
736
+
737
+ # Always clean up service connection, regardless of status
738
+ if self._nacos_ai_service is not None:
739
+ try:
740
+ await self._nacos_ai_service.shutdown()
741
+ except Exception:
742
+ logger.debug(
743
+ "[NacosRegistry] Error closing service during cleanup",
744
+ exc_info=True,
745
+ )
746
+ finally:
747
+ self._nacos_ai_service = None
748
+
749
+ logger.info("[NacosRegistry] Cleanup completed")