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.
- paglets/__init__.py +3 -0
- paglets/config/__init__.py +3 -0
- paglets/config/defaults/__init__.py +3 -0
- paglets/config/defaults/launch.toml +23 -0
- paglets/config/startup.py +431 -0
- paglets/core/__init__.py +3 -0
- paglets/core/agent.py +614 -0
- paglets/core/context_events.py +148 -0
- paglets/core/errors.py +59 -0
- paglets/core/events.py +46 -0
- paglets/core/itinerary.py +223 -0
- paglets/core/messages.py +229 -0
- paglets/core/runtime_values.py +55 -0
- paglets/core/wire.py +9 -0
- paglets/examples/__init__.py +3 -0
- paglets/examples/compute/__init__.py +42 -0
- paglets/examples/compute/agent.py +1279 -0
- paglets/examples/compute/chudnovsky.py +221 -0
- paglets/examples/compute/cli.py +261 -0
- paglets/examples/compute/models.py +111 -0
- paglets/examples/mesh_benchmark/__init__.py +59 -0
- paglets/examples/mesh_benchmark/agent.py +479 -0
- paglets/examples/mesh_benchmark/analysis.py +304 -0
- paglets/examples/mesh_benchmark/cli.py +327 -0
- paglets/examples/mesh_benchmark/models.py +123 -0
- paglets/examples/mesh_info/__init__.py +43 -0
- paglets/examples/mesh_info/agent.py +466 -0
- paglets/examples/mesh_info/cli.py +197 -0
- paglets/examples/performance/__init__.py +36 -0
- paglets/examples/performance/agent.py +196 -0
- paglets/examples/performance/cli.py +290 -0
- paglets/examples/performance/kernels.py +549 -0
- paglets/examples/performance/models.py +98 -0
- paglets/examples/search/__init__.py +25 -0
- paglets/examples/search/agent.py +287 -0
- paglets/examples/search/cli.py +369 -0
- paglets/examples/search/local_search.py +555 -0
- paglets/examples/search/models.py +103 -0
- paglets/examples/system_info/__init__.py +47 -0
- paglets/examples/system_info/agent.py +503 -0
- paglets/examples/system_info/cli.py +215 -0
- paglets/persistence/__init__.py +3 -0
- paglets/persistence/persistency.py +131 -0
- paglets/persistence/storage.py +92 -0
- paglets/remote/__init__.py +3 -0
- paglets/remote/admin.py +457 -0
- paglets/remote/client.py +126 -0
- paglets/remote/mesh.py +625 -0
- paglets/remote/proxy.py +230 -0
- paglets/remote/references.py +36 -0
- paglets/remote/transfer.py +59 -0
- paglets/remote/transport.py +394 -0
- paglets/runtime/__init__.py +3 -0
- paglets/runtime/binding.py +61 -0
- paglets/runtime/child_bootstrap.py +227 -0
- paglets/runtime/child_calls.py +258 -0
- paglets/runtime/child_endpoint.py +121 -0
- paglets/runtime/child_facade.py +424 -0
- paglets/runtime/envelope.py +59 -0
- paglets/runtime/host.py +1142 -0
- paglets/runtime/http_api.py +298 -0
- paglets/runtime/inactive_records.py +180 -0
- paglets/runtime/lifecycle.py +552 -0
- paglets/runtime/mailbox.py +147 -0
- paglets/runtime/process_controller.py +343 -0
- paglets/runtime/process_protocol.py +163 -0
- paglets/runtime/process_runtime.py +12 -0
- paglets/runtime/relay.py +611 -0
- paglets/runtime/resident_services.py +420 -0
- paglets/runtime/resources.py +69 -0
- paglets/serialization/__init__.py +3 -0
- paglets/serialization/codec.py +191 -0
- paglets/services/__init__.py +3 -0
- paglets/services/contracts.py +390 -0
- paglets/services/resident.py +69 -0
- paglets/tooling/__init__.py +3 -0
- paglets/tooling/cli.py +332 -0
- paglets/tooling/discovery.py +168 -0
- paglets/tooling/git_update.py +493 -0
- paglets-0.1.0.dist-info/METADATA +163 -0
- paglets-0.1.0.dist-info/RECORD +84 -0
- paglets-0.1.0.dist-info/WHEEL +4 -0
- paglets-0.1.0.dist-info/entry_points.txt +8 -0
- 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}")
|