paglets 0.1.0__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 (84) hide show
  1. paglets/__init__.py +3 -0
  2. paglets/config/__init__.py +3 -0
  3. paglets/config/defaults/__init__.py +3 -0
  4. paglets/config/defaults/launch.toml +23 -0
  5. paglets/config/startup.py +431 -0
  6. paglets/core/__init__.py +3 -0
  7. paglets/core/agent.py +614 -0
  8. paglets/core/context_events.py +148 -0
  9. paglets/core/errors.py +59 -0
  10. paglets/core/events.py +46 -0
  11. paglets/core/itinerary.py +223 -0
  12. paglets/core/messages.py +229 -0
  13. paglets/core/runtime_values.py +55 -0
  14. paglets/core/wire.py +9 -0
  15. paglets/examples/__init__.py +3 -0
  16. paglets/examples/compute/__init__.py +42 -0
  17. paglets/examples/compute/agent.py +1279 -0
  18. paglets/examples/compute/chudnovsky.py +221 -0
  19. paglets/examples/compute/cli.py +261 -0
  20. paglets/examples/compute/models.py +111 -0
  21. paglets/examples/mesh_benchmark/__init__.py +59 -0
  22. paglets/examples/mesh_benchmark/agent.py +479 -0
  23. paglets/examples/mesh_benchmark/analysis.py +304 -0
  24. paglets/examples/mesh_benchmark/cli.py +327 -0
  25. paglets/examples/mesh_benchmark/models.py +123 -0
  26. paglets/examples/mesh_info/__init__.py +43 -0
  27. paglets/examples/mesh_info/agent.py +466 -0
  28. paglets/examples/mesh_info/cli.py +197 -0
  29. paglets/examples/performance/__init__.py +36 -0
  30. paglets/examples/performance/agent.py +196 -0
  31. paglets/examples/performance/cli.py +290 -0
  32. paglets/examples/performance/kernels.py +549 -0
  33. paglets/examples/performance/models.py +98 -0
  34. paglets/examples/search/__init__.py +25 -0
  35. paglets/examples/search/agent.py +287 -0
  36. paglets/examples/search/cli.py +369 -0
  37. paglets/examples/search/local_search.py +555 -0
  38. paglets/examples/search/models.py +103 -0
  39. paglets/examples/system_info/__init__.py +47 -0
  40. paglets/examples/system_info/agent.py +503 -0
  41. paglets/examples/system_info/cli.py +215 -0
  42. paglets/persistence/__init__.py +3 -0
  43. paglets/persistence/persistency.py +131 -0
  44. paglets/persistence/storage.py +92 -0
  45. paglets/remote/__init__.py +3 -0
  46. paglets/remote/admin.py +457 -0
  47. paglets/remote/client.py +126 -0
  48. paglets/remote/mesh.py +625 -0
  49. paglets/remote/proxy.py +230 -0
  50. paglets/remote/references.py +36 -0
  51. paglets/remote/transfer.py +59 -0
  52. paglets/remote/transport.py +394 -0
  53. paglets/runtime/__init__.py +3 -0
  54. paglets/runtime/binding.py +61 -0
  55. paglets/runtime/child_bootstrap.py +227 -0
  56. paglets/runtime/child_calls.py +258 -0
  57. paglets/runtime/child_endpoint.py +121 -0
  58. paglets/runtime/child_facade.py +424 -0
  59. paglets/runtime/envelope.py +59 -0
  60. paglets/runtime/host.py +1142 -0
  61. paglets/runtime/http_api.py +298 -0
  62. paglets/runtime/inactive_records.py +180 -0
  63. paglets/runtime/lifecycle.py +552 -0
  64. paglets/runtime/mailbox.py +147 -0
  65. paglets/runtime/process_controller.py +343 -0
  66. paglets/runtime/process_protocol.py +163 -0
  67. paglets/runtime/process_runtime.py +12 -0
  68. paglets/runtime/relay.py +611 -0
  69. paglets/runtime/resident_services.py +420 -0
  70. paglets/runtime/resources.py +69 -0
  71. paglets/serialization/__init__.py +3 -0
  72. paglets/serialization/codec.py +191 -0
  73. paglets/services/__init__.py +3 -0
  74. paglets/services/contracts.py +390 -0
  75. paglets/services/resident.py +69 -0
  76. paglets/tooling/__init__.py +3 -0
  77. paglets/tooling/cli.py +332 -0
  78. paglets/tooling/discovery.py +168 -0
  79. paglets/tooling/git_update.py +493 -0
  80. paglets-0.1.0.dist-info/METADATA +163 -0
  81. paglets-0.1.0.dist-info/RECORD +84 -0
  82. paglets-0.1.0.dist-info/WHEEL +4 -0
  83. paglets-0.1.0.dist-info/entry_points.txt +8 -0
  84. paglets-0.1.0.dist-info/licenses/LICENSE +21 -0
paglets/core/agent.py ADDED
@@ -0,0 +1,614 @@
1
+ # Copyright (c) 2026 by C. Klukas.
2
+ # Licensed under the MIT License. See LICENSE for details.
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ import time
7
+ import uuid
8
+ from collections.abc import Callable, Iterator
9
+ from contextlib import contextmanager
10
+ from dataclasses import is_dataclass
11
+ from functools import wraps
12
+ from typing import TYPE_CHECKING, Any, ClassVar, Concatenate, Generic, ParamSpec, TypeVar
13
+
14
+ from paglets.core.errors import HostError, NotHandledError
15
+ from paglets.core.events import CloneEvent, CreationEvent, MobilityEvent, PersistencyEvent
16
+ from paglets.core.messages import Message, ReplySet
17
+ from paglets.core.runtime_values import ServiceScope
18
+ from paglets.persistence.persistency import DeactivationPolicy, DeactivationRequest
19
+ from paglets.runtime.resources import ResourceRegistry
20
+
21
+ if TYPE_CHECKING: # pragma: no cover
22
+ from pathlib import Path
23
+
24
+ from paglets.persistence.storage import ManagedStorage
25
+ from paglets.remote.mesh import HostRef
26
+ from paglets.remote.proxy import PagletProxy
27
+ from paglets.remote.references import PagletProxyRef
28
+ from paglets.remote.transfer import TransferTicket
29
+ from paglets.runtime.host import Host
30
+ from paglets.services.contracts import ServiceContract, ServiceHandle, ServiceOperation, ServiceRecord
31
+ from paglets.services.resident import ServiceLease
32
+
33
+
34
+ ACTIVE = 0x1
35
+ INACTIVE = 0x1 << 1
36
+
37
+
38
+ class PagletState:
39
+ """Marker base class for dataclass state objects.
40
+
41
+ Subclass this with ``@dataclass``. Only this state object moves. Everything
42
+ stored directly on the paglet instance is transient runtime state.
43
+ """
44
+
45
+
46
+ class _NotHandled:
47
+ pass
48
+
49
+
50
+ NOT_HANDLED = _NotHandled()
51
+
52
+
53
+ StateT = TypeVar("StateT", bound=PagletState)
54
+ PagletT = TypeVar("PagletT", bound="Paglet[Any]")
55
+ P = ParamSpec("P")
56
+ ReturnT = TypeVar("ReturnT")
57
+
58
+
59
+ def state_locked(method: Callable[Concatenate[PagletT, P], ReturnT]) -> Callable[Concatenate[PagletT, P], ReturnT]:
60
+ """Run a paglet method under the paglet's reentrant state lock."""
61
+
62
+ @wraps(method)
63
+ def wrapper(self: PagletT, *args: P.args, **kwargs: P.kwargs) -> ReturnT:
64
+ with self.locked():
65
+ return method(self, *args, **kwargs)
66
+
67
+ return wrapper
68
+
69
+
70
+ class PagletContext:
71
+ """Host-provided environment visible to a running paglet."""
72
+
73
+ def __init__(self, host: Host, agent_id: str | None = None):
74
+ self._host = host
75
+ self._agent_id = agent_id
76
+
77
+ @property
78
+ def name(self) -> str:
79
+ return self._host.name
80
+
81
+ @property
82
+ def address(self) -> str:
83
+ return self._host.address
84
+
85
+ @property
86
+ def host(self) -> Host:
87
+ return self._host
88
+
89
+ @property
90
+ def agent_id(self) -> str | None:
91
+ return self._agent_id
92
+
93
+ def get_proxy(self, agent_id: str, host_url: str | None = None) -> PagletProxy | None:
94
+ if host_url is None or host_url.rstrip("/") == self.address.rstrip("/"):
95
+ return self._host.get_proxy(agent_id)
96
+ from paglets.remote.proxy import PagletProxy
97
+
98
+ return PagletProxy(host_url=host_url, agent_id=agent_id, client=self._host.client)
99
+
100
+ def get_proxies(self, state: int = ACTIVE) -> list[PagletProxy]:
101
+ return self._host.get_proxies(state)
102
+
103
+ def get_property(self, key: str, default: Any = None) -> Any:
104
+ return self._host.get_property(key, default)
105
+
106
+ def set_property(self, key: str, value: Any) -> None:
107
+ self._host.set_property(key, value)
108
+
109
+ def create_paglet(
110
+ self,
111
+ agent_cls: type[Paglet],
112
+ state: PagletState | None = None,
113
+ *,
114
+ init: Any = None,
115
+ host_url: str | None = None,
116
+ ) -> PagletProxy:
117
+ if host_url is not None and host_url.rstrip("/") != self.address.rstrip("/"):
118
+ return self._host.create_remote(host_url, agent_cls, state, init=init)
119
+ return self._host.create(agent_cls, state, init=init)
120
+
121
+ def dispatch(self, agent_id: str, target: str | TransferTicket) -> PagletProxy:
122
+ return self._host.dispatch(agent_id, target)
123
+
124
+ def clone(self, agent_id: str, target: str | TransferTicket | None = None) -> PagletProxy:
125
+ return self._host.clone(agent_id, target=target)
126
+
127
+ def deactivate(
128
+ self,
129
+ agent_id: str,
130
+ request: DeactivationRequest | None = None,
131
+ ) -> PagletProxy:
132
+ return self._host.deactivate(agent_id, request=request)
133
+
134
+ def available_hosts(self, *, online_only: bool = True, include_self: bool = True) -> list[HostRef]:
135
+ return self._host.mesh.hosts(online_only=online_only, include_self=include_self)
136
+
137
+ def host_status(self, name_or_url: str) -> HostRef | None:
138
+ return self._host.mesh.lookup(name_or_url)
139
+
140
+ def is_host_online(self, name_or_url: str) -> bool:
141
+ return self._host.mesh.is_online(name_or_url)
142
+
143
+ def wait_for_host(self, name_or_url: str, *, timeout: float = 10.0, interval: float = 0.25) -> HostRef:
144
+ return self._host.mesh.wait_for_host(name_or_url, timeout=timeout, interval=interval)
145
+
146
+ def dispatch_to(self, agent_id: str, name_or_url: str) -> PagletProxy:
147
+ return self.dispatch(agent_id, self._host.mesh.resolve_url(name_or_url))
148
+
149
+ def clone_to(self, agent_id: str, name_or_url: str) -> PagletProxy:
150
+ return self.clone(agent_id, self._host.mesh.resolve_url(name_or_url))
151
+
152
+ def send(self, target_agent_id: str, message: Message, *, host_url: str | None = None) -> Any:
153
+ proxy = self.get_proxy(target_agent_id, host_url)
154
+ if proxy is None:
155
+ raise HostError(f"No such local paglet: {target_agent_id}")
156
+ if message.sender is None:
157
+ message.sender = self.address
158
+ return proxy.send(message)
159
+
160
+ def multicast(
161
+ self, kind: str | Message, args: dict[str, Any] | None = None, *, exclude: set[str] | None = None
162
+ ) -> ReplySet:
163
+ return self._host.multicast_message(kind, args, exclude=exclude)
164
+
165
+ def advertise_service(
166
+ self,
167
+ name: str,
168
+ *,
169
+ capabilities: list[str] | tuple[str, ...] | None = None,
170
+ metadata: dict[str, Any] | None = None,
171
+ scope: ServiceScope = ServiceScope.LOCAL,
172
+ ttl: float | None = None,
173
+ agent_id: str | None = None,
174
+ ) -> ServiceRecord:
175
+ owner_id = agent_id or self._agent_id
176
+ if owner_id is None:
177
+ raise HostError("advertise_service requires an attached paglet or explicit agent_id")
178
+ return self._host.advertise_service(
179
+ owner_id,
180
+ name,
181
+ capabilities=capabilities,
182
+ metadata=metadata,
183
+ scope=scope,
184
+ ttl=ttl,
185
+ )
186
+
187
+ def unadvertise_service(self, name: str, *, agent_id: str | None = None) -> list[ServiceRecord]:
188
+ owner_id = agent_id or self._agent_id
189
+ if owner_id is None:
190
+ raise HostError("unadvertise_service requires an attached paglet or explicit agent_id")
191
+ return self._host.unadvertise_service(name, agent_id=owner_id)
192
+
193
+ def lookup_service(
194
+ self,
195
+ name: str,
196
+ *,
197
+ capability: str | None = None,
198
+ scope: ServiceScope = ServiceScope.LOCAL,
199
+ ) -> PagletProxyRef | None:
200
+ record = self._host.lookup_service(name, capability=capability, scope=scope)
201
+ return record.proxy if record is not None else None
202
+
203
+ def lookup_services(
204
+ self,
205
+ name: str | None = None,
206
+ *,
207
+ capability: str | None = None,
208
+ scope: ServiceScope = ServiceScope.LOCAL,
209
+ ) -> list[ServiceRecord]:
210
+ return self._host.lookup_services(name, capability=capability, scope=scope)
211
+
212
+ def advertise_contract(
213
+ self,
214
+ contract: ServiceContract,
215
+ *,
216
+ scope: ServiceScope = ServiceScope.LOCAL,
217
+ ttl: float | None = None,
218
+ metadata: dict[str, Any] | None = None,
219
+ agent_id: str | None = None,
220
+ ) -> ServiceRecord:
221
+ owner_id = agent_id or self._agent_id
222
+ if owner_id is None:
223
+ raise HostError("advertise_contract requires an attached paglet or explicit agent_id")
224
+ return self.advertise_service(
225
+ contract.name,
226
+ capabilities=contract.capabilities,
227
+ metadata=contract.advertise_metadata(metadata),
228
+ scope=scope,
229
+ ttl=ttl,
230
+ agent_id=owner_id,
231
+ )
232
+
233
+ def lookup_contract(
234
+ self,
235
+ contract: ServiceContract,
236
+ *,
237
+ operation: ServiceOperation[Any, Any] | None = None,
238
+ scope: ServiceScope = ServiceScope.LOCAL,
239
+ ) -> ServiceHandle | None:
240
+ handles = self.lookup_contracts(contract, operation=operation, scope=scope)
241
+ return handles[0] if handles else None
242
+
243
+ def lookup_contracts(
244
+ self,
245
+ contract: ServiceContract,
246
+ *,
247
+ operation: ServiceOperation[Any, Any] | None = None,
248
+ scope: ServiceScope = ServiceScope.LOCAL,
249
+ ) -> list[ServiceHandle]:
250
+ from paglets.services.contracts import ServiceHandle
251
+
252
+ if operation is not None:
253
+ operation = contract.require_operation(operation)
254
+ capability = operation.name if operation is not None else None
255
+ return [
256
+ ServiceHandle(contract, record, self)
257
+ for record in self.lookup_services(contract.name, capability=capability, scope=scope)
258
+ if contract.matches_record(record)
259
+ ]
260
+
261
+ def require_contract(
262
+ self,
263
+ contract: ServiceContract,
264
+ *,
265
+ operation: ServiceOperation[Any, Any] | None = None,
266
+ scope: ServiceScope = ServiceScope.LOCAL,
267
+ ) -> ServiceHandle:
268
+ from paglets.core.errors import ServiceNotFoundError
269
+
270
+ handle = self.lookup_contract(contract, operation=operation, scope=scope)
271
+ if handle is None:
272
+ operation_text = f" operation {operation.name!r}" if operation is not None else ""
273
+ contract_text = f"contract {contract.name!r} version {contract.version!r}{operation_text}"
274
+ raise ServiceNotFoundError(f"No service {contract_text} found in {scope} scope")
275
+ return handle
276
+
277
+ def lease_contract(
278
+ self,
279
+ contract: ServiceContract,
280
+ *,
281
+ operation: ServiceOperation[Any, Any] | None = None,
282
+ scope: ServiceScope = ServiceScope.LOCAL,
283
+ ttl: float = 60.0,
284
+ ) -> ServiceLease:
285
+ handle = self.require_contract(contract, operation=operation, scope=scope)
286
+ lease = self._host.lease_service_handle(handle, ttl=ttl)
287
+ if self._agent_id is not None:
288
+ self._host.resources_for(self._agent_id).register(
289
+ f"service-lease:{lease.lease_id}",
290
+ lease.release,
291
+ suppress=True,
292
+ )
293
+ return lease
294
+
295
+ def resources(self, agent_id: str | None = None) -> ResourceRegistry:
296
+ owner_id = agent_id or self._agent_id
297
+ if owner_id is None:
298
+ raise HostError("resources requires an attached paglet or explicit agent_id")
299
+ return self._host.resources_for(owner_id)
300
+
301
+ def work_dir(self, *, create: bool = True, agent_id: str | None = None) -> Path:
302
+ owner_id = agent_id or self._agent_id
303
+ if owner_id is None:
304
+ raise HostError("work_dir requires an attached paglet or explicit agent_id")
305
+ return self._host.work_dir_for(owner_id, create=create)
306
+
307
+ def persistent_storage(
308
+ self,
309
+ *,
310
+ quota_bytes: int | None = None,
311
+ agent_id: str | None = None,
312
+ ) -> ManagedStorage:
313
+ owner_id = agent_id or self._agent_id
314
+ if owner_id is None:
315
+ raise HostError("persistent_storage requires an attached paglet or explicit agent_id")
316
+ return self._host.persistent_storage_for(owner_id, quota_bytes=quota_bytes)
317
+
318
+
319
+ class Paglet(Generic[StateT]):
320
+ """Base class for mobile Python objects.
321
+
322
+ Subclasses set ``State`` to a dataclass type and override lifecycle hooks.
323
+ The runtime instantiates paglets on each host from class path + dataclass
324
+ state, mirroring Aglets' mobile object plus event system without moving a
325
+ call stack.
326
+ """
327
+
328
+ State: ClassVar[type[StateT]]
329
+ ACTIVE: ClassVar[int] = ACTIVE
330
+ INACTIVE: ClassVar[int] = INACTIVE
331
+ MAILBOX_WORKERS: ClassVar[int] = 4
332
+
333
+ def __init__(self, state: StateT | None = None, *, agent_id: str | None = None):
334
+ state_cls = self.state_class()
335
+ if state is None:
336
+ state = state_cls() # type: ignore[call-arg]
337
+ if not is_dataclass(state):
338
+ raise HostError(f"{self.__class__.__name__}.State must be a dataclass state object")
339
+ self.agent_id = agent_id or uuid.uuid4().hex
340
+ self.state: StateT = state
341
+ self._state_lock = threading.RLock()
342
+ self._state_condition = threading.Condition(self._state_lock)
343
+ self._context: PagletContext | None = None
344
+ self._last_proxy: PagletProxy | None = None
345
+ self.resources = ResourceRegistry()
346
+
347
+ @classmethod
348
+ def state_class(cls) -> type[StateT]:
349
+ state_cls = getattr(cls, "State", None)
350
+ if state_cls is None:
351
+ raise HostError(f"{cls.__name__} must define a dataclass State class")
352
+ if not is_dataclass(state_cls):
353
+ raise HostError(f"{cls.__name__}.State must be decorated with @dataclass")
354
+ return state_cls
355
+
356
+ @property
357
+ def context(self) -> PagletContext:
358
+ if self._context is None:
359
+ raise HostError("Paglet is not attached to a host context")
360
+ return self._context
361
+
362
+ def _attach(self, context: PagletContext) -> None:
363
+ self._context = context
364
+
365
+ @contextmanager
366
+ def locked(self) -> Iterator[None]:
367
+ """Enter the paglet's reentrant lock for agent-local critical sections."""
368
+
369
+ with self._state_lock:
370
+ yield
371
+
372
+ @contextmanager
373
+ def locked_state(self) -> Iterator[StateT]:
374
+ """Yield this paglet's dataclass state under the paglet lock."""
375
+
376
+ with self._state_lock:
377
+ yield self.state
378
+
379
+ def wait_state(self, predicate: Callable[[StateT], bool], timeout: float | None = None) -> bool:
380
+ """Wait until ``predicate(state)`` is true.
381
+
382
+ This is for coordination between handlers/background work that mutate
383
+ paglet state and another handler waiting for that state to change. It
384
+ does not replace normal message delivery; incoming messages still call
385
+ ``handle_message`` through the paglet mailbox.
386
+ """
387
+
388
+ deadline = None if timeout is None else time.monotonic() + max(0.0, timeout)
389
+
390
+ with self._state_condition:
391
+ if predicate(self.state):
392
+ return True
393
+ while True:
394
+ if deadline is None:
395
+ remaining = None
396
+ else:
397
+ remaining = deadline - time.monotonic()
398
+ if remaining <= 0:
399
+ return bool(predicate(self.state))
400
+ self._state_condition.wait(remaining)
401
+ if predicate(self.state):
402
+ return True
403
+
404
+ def notify_state_changed(self) -> None:
405
+ """Wake one waiter blocked in :meth:`wait_state`."""
406
+
407
+ with self._state_condition:
408
+ self._state_condition.notify(1)
409
+
410
+ def notify_all_state_changed(self) -> None:
411
+ """Wake all waiters blocked in :meth:`wait_state`."""
412
+
413
+ with self._state_condition:
414
+ self._state_condition.notify_all()
415
+
416
+ # Convenience operations available from inside lifecycle/message handlers.
417
+ def dispatch(self, target: str | TransferTicket) -> PagletProxy:
418
+ proxy = self.context.dispatch(self.agent_id, target)
419
+ self._last_proxy = proxy
420
+ return proxy
421
+
422
+ def clone(self, target: str | TransferTicket | None = None) -> PagletProxy:
423
+ proxy = self.context.clone(self.agent_id, target)
424
+ self._last_proxy = proxy
425
+ return proxy
426
+
427
+ def dispatch_to(self, name_or_url: str) -> PagletProxy:
428
+ proxy = self.context.dispatch_to(self.agent_id, name_or_url)
429
+ self._last_proxy = proxy
430
+ return proxy
431
+
432
+ def clone_to(self, name_or_url: str) -> PagletProxy:
433
+ proxy = self.context.clone_to(self.agent_id, name_or_url)
434
+ self._last_proxy = proxy
435
+ return proxy
436
+
437
+ def deactivate(
438
+ self,
439
+ *,
440
+ reason: str = "deactivate",
441
+ policy: DeactivationPolicy | None = None,
442
+ metadata: dict[str, Any] | None = None,
443
+ ) -> PagletProxy:
444
+ proxy = self.context.deactivate(
445
+ self.agent_id,
446
+ DeactivationRequest(
447
+ reason=reason,
448
+ source="self",
449
+ policy=policy,
450
+ metadata=metadata or {},
451
+ ),
452
+ )
453
+ self._last_proxy = proxy
454
+ return proxy
455
+
456
+ def send(self, target_agent_id: str, message: Message, *, host_url: str | None = None) -> Any:
457
+ return self.context.send(target_agent_id, message, host_url=host_url)
458
+
459
+ def multicast(
460
+ self, kind: str | Message, args: dict[str, Any] | None = None, *, include_self: bool = True
461
+ ) -> ReplySet:
462
+ exclude = None if include_self else {self.agent_id}
463
+ return self.context.multicast(kind, args, exclude=exclude)
464
+
465
+ def wait_message(self, timeout: float | None = None) -> bool:
466
+ return self.context.host.wait_message(self.agent_id, timeout=timeout)
467
+
468
+ def notify_message(self) -> None:
469
+ self.context.host.notify_message(self.agent_id)
470
+
471
+ def notify_all_messages(self) -> None:
472
+ self.context.host.notify_all_messages(self.agent_id)
473
+
474
+ def advertise_service(
475
+ self,
476
+ name: str,
477
+ *,
478
+ capabilities: list[str] | tuple[str, ...] | None = None,
479
+ metadata: dict[str, Any] | None = None,
480
+ scope: ServiceScope = ServiceScope.LOCAL,
481
+ ttl: float | None = None,
482
+ ) -> ServiceRecord:
483
+ return self.context.advertise_service(
484
+ name,
485
+ capabilities=capabilities,
486
+ metadata=metadata,
487
+ scope=scope,
488
+ ttl=ttl,
489
+ agent_id=self.agent_id,
490
+ )
491
+
492
+ def unadvertise_service(self, name: str) -> list[ServiceRecord]:
493
+ return self.context.unadvertise_service(name, agent_id=self.agent_id)
494
+
495
+ def lookup_service(
496
+ self,
497
+ name: str,
498
+ *,
499
+ capability: str | None = None,
500
+ scope: ServiceScope = ServiceScope.LOCAL,
501
+ ) -> PagletProxyRef | None:
502
+ return self.context.lookup_service(name, capability=capability, scope=scope)
503
+
504
+ def lookup_services(
505
+ self,
506
+ name: str | None = None,
507
+ *,
508
+ capability: str | None = None,
509
+ scope: ServiceScope = ServiceScope.LOCAL,
510
+ ) -> list[ServiceRecord]:
511
+ return self.context.lookup_services(name, capability=capability, scope=scope)
512
+
513
+ def advertise_contract(
514
+ self,
515
+ contract: ServiceContract,
516
+ *,
517
+ scope: ServiceScope = ServiceScope.LOCAL,
518
+ ttl: float | None = None,
519
+ metadata: dict[str, Any] | None = None,
520
+ ) -> ServiceRecord:
521
+ return self.context.advertise_contract(
522
+ contract,
523
+ scope=scope,
524
+ ttl=ttl,
525
+ metadata=metadata,
526
+ agent_id=self.agent_id,
527
+ )
528
+
529
+ def lookup_contract(
530
+ self,
531
+ contract: ServiceContract,
532
+ *,
533
+ operation: ServiceOperation[Any, Any] | None = None,
534
+ scope: ServiceScope = ServiceScope.LOCAL,
535
+ ) -> ServiceHandle | None:
536
+ return self.context.lookup_contract(contract, operation=operation, scope=scope)
537
+
538
+ def lookup_contracts(
539
+ self,
540
+ contract: ServiceContract,
541
+ *,
542
+ operation: ServiceOperation[Any, Any] | None = None,
543
+ scope: ServiceScope = ServiceScope.LOCAL,
544
+ ) -> list[ServiceHandle]:
545
+ return self.context.lookup_contracts(contract, operation=operation, scope=scope)
546
+
547
+ def require_contract(
548
+ self,
549
+ contract: ServiceContract,
550
+ *,
551
+ operation: ServiceOperation[Any, Any] | None = None,
552
+ scope: ServiceScope = ServiceScope.LOCAL,
553
+ ) -> ServiceHandle:
554
+ return self.context.require_contract(contract, operation=operation, scope=scope)
555
+
556
+ def lease_contract(
557
+ self,
558
+ contract: ServiceContract,
559
+ *,
560
+ operation: ServiceOperation[Any, Any] | None = None,
561
+ scope: ServiceScope = ServiceScope.LOCAL,
562
+ ttl: float = 60.0,
563
+ ) -> ServiceLease:
564
+ return self.context.lease_contract(contract, operation=operation, scope=scope, ttl=ttl)
565
+
566
+ def work_dir(self, *, create: bool = True) -> Path:
567
+ return self.context.work_dir(create=create, agent_id=self.agent_id)
568
+
569
+ def persistent_storage(self, *, quota_bytes: int | None = None) -> ManagedStorage:
570
+ return self.context.persistent_storage(quota_bytes=quota_bytes, agent_id=self.agent_id)
571
+
572
+ @staticmethod
573
+ def not_handled() -> _NotHandled:
574
+ return NOT_HANDLED
575
+
576
+ # Lifecycle/event hooks. Override these in subclasses.
577
+ def on_creation(self, event: CreationEvent) -> None:
578
+ pass
579
+
580
+ def on_dispatching(self, event: MobilityEvent) -> None:
581
+ pass
582
+
583
+ def on_arrival(self, event: MobilityEvent) -> None:
584
+ pass
585
+
586
+ def on_reverting(self, event: MobilityEvent) -> None:
587
+ pass
588
+
589
+ def on_cloning(self, event: CloneEvent) -> None:
590
+ pass
591
+
592
+ def on_clone(self, event: CloneEvent) -> None:
593
+ pass
594
+
595
+ def on_cloned(self, event: CloneEvent) -> None:
596
+ pass
597
+
598
+ def on_deactivating(self, event: PersistencyEvent) -> None:
599
+ pass
600
+
601
+ def on_activation(self, event: PersistencyEvent) -> None:
602
+ pass
603
+
604
+ def on_disposing(self, event: PersistencyEvent) -> None:
605
+ pass
606
+
607
+ def deactivation_policy(self, request: DeactivationRequest) -> DeactivationPolicy:
608
+ return request.policy or DeactivationPolicy()
609
+
610
+ def run(self) -> None:
611
+ pass
612
+
613
+ def handle_message(self, message: Message) -> Any:
614
+ raise NotHandledError(f"{self.__class__.__name__} did not handle {message.kind!r}")