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/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[launch]
|
|
2
|
+
demo_config_id = "paglets-default-launch"
|
|
3
|
+
demo_config_version = "4"
|
|
4
|
+
|
|
5
|
+
[[resident_services]]
|
|
6
|
+
class = "paglets.examples.system_info.agent:ServerInfoAgent"
|
|
7
|
+
enabled = true
|
|
8
|
+
agent_id = "service.server-info"
|
|
9
|
+
singleton = true
|
|
10
|
+
lifecycle = "lazy"
|
|
11
|
+
scope = "mesh"
|
|
12
|
+
idle_timeout = 30.0
|
|
13
|
+
state = { service_scope = "mesh" }
|
|
14
|
+
|
|
15
|
+
[[resident_services]]
|
|
16
|
+
class = "paglets.examples.mesh_info.agent:MeshInfoAgent"
|
|
17
|
+
enabled = true
|
|
18
|
+
agent_id = "service.mesh-info"
|
|
19
|
+
singleton = true
|
|
20
|
+
lifecycle = "eager"
|
|
21
|
+
scope = "mesh"
|
|
22
|
+
idle_timeout = 0.0
|
|
23
|
+
state = { service_scope = "mesh" }
|
|
@@ -0,0 +1,431 @@
|
|
|
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 sys
|
|
6
|
+
import tomllib
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from importlib import resources
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, TextIO
|
|
13
|
+
|
|
14
|
+
from paglets.core.errors import HostError, SerializationError
|
|
15
|
+
from paglets.core.runtime_values import (
|
|
16
|
+
LaunchConfigSyncAction,
|
|
17
|
+
ResidentLifecycle,
|
|
18
|
+
ServiceScope,
|
|
19
|
+
enum_from_wire,
|
|
20
|
+
require_enum,
|
|
21
|
+
)
|
|
22
|
+
from paglets.serialization.codec import dataclass_from_wire, resolve_qualified_name
|
|
23
|
+
from paglets.services.resident import ResidentServiceSpec
|
|
24
|
+
|
|
25
|
+
DEFAULT_LAUNCH_CONFIG_PATH = Path.home() / ".paglets" / "launch.toml"
|
|
26
|
+
DEFAULT_DEMO_CONFIG_ID = "paglets-default-launch"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class AutoStartSpec:
|
|
31
|
+
"""Class-level marker for agents that can be started from launch config."""
|
|
32
|
+
|
|
33
|
+
alias: str
|
|
34
|
+
agent_id: str | None = None
|
|
35
|
+
singleton: bool = True
|
|
36
|
+
state: dict[str, Any] = field(default_factory=dict)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class StartupAgentConfig:
|
|
41
|
+
"""One launch-config entry describing an agent to start."""
|
|
42
|
+
|
|
43
|
+
use: str | None = None
|
|
44
|
+
class_name: str | None = None
|
|
45
|
+
enabled: bool = True
|
|
46
|
+
agent_id: str | None = None
|
|
47
|
+
singleton: bool = True
|
|
48
|
+
state: dict[str, Any] = field(default_factory=dict)
|
|
49
|
+
init: Any = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class ResidentServiceConfig:
|
|
54
|
+
"""One launch-config entry describing a managed resident service."""
|
|
55
|
+
|
|
56
|
+
use: str | None = None
|
|
57
|
+
class_name: str | None = None
|
|
58
|
+
service_name: str | None = None
|
|
59
|
+
enabled: bool = True
|
|
60
|
+
agent_id: str | None = None
|
|
61
|
+
singleton: bool = True
|
|
62
|
+
lifecycle: ResidentLifecycle | None = None
|
|
63
|
+
scope: ServiceScope | None = None
|
|
64
|
+
idle_timeout: float | None = None
|
|
65
|
+
state: dict[str, Any] = field(default_factory=dict)
|
|
66
|
+
init: Any = None
|
|
67
|
+
|
|
68
|
+
def __post_init__(self) -> None:
|
|
69
|
+
if self.lifecycle is not None:
|
|
70
|
+
require_enum(self.lifecycle, ResidentLifecycle, "lifecycle")
|
|
71
|
+
if self.scope is not None:
|
|
72
|
+
require_enum(self.scope, ServiceScope, "scope")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True, slots=True)
|
|
76
|
+
class LaunchConfig:
|
|
77
|
+
"""Parsed paglets launch config."""
|
|
78
|
+
|
|
79
|
+
path: Path | None = None
|
|
80
|
+
demo_config_id: str | None = None
|
|
81
|
+
demo_config_version: str | None = None
|
|
82
|
+
sync_demo_config: bool = True
|
|
83
|
+
startup_agents: tuple[StartupAgentConfig, ...] = ()
|
|
84
|
+
resident_services: tuple[ResidentServiceConfig, ...] = ()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True, slots=True)
|
|
88
|
+
class LaunchConfigSyncResult:
|
|
89
|
+
"""Result of syncing the bundled demo launch config to the user path."""
|
|
90
|
+
|
|
91
|
+
action: LaunchConfigSyncAction
|
|
92
|
+
path: Path
|
|
93
|
+
message: str
|
|
94
|
+
backup_path: Path | None = None
|
|
95
|
+
|
|
96
|
+
def __post_init__(self) -> None:
|
|
97
|
+
require_enum(self.action, LaunchConfigSyncAction, "action")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True, slots=True)
|
|
101
|
+
class ResolvedStartupAgent:
|
|
102
|
+
agent_cls: type[Any]
|
|
103
|
+
state: Any
|
|
104
|
+
agent_id: str | None
|
|
105
|
+
singleton: bool
|
|
106
|
+
init: Any
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True, slots=True)
|
|
110
|
+
class ResolvedResidentService:
|
|
111
|
+
agent_cls: type[Any]
|
|
112
|
+
state: Any
|
|
113
|
+
agent_id: str
|
|
114
|
+
singleton: bool
|
|
115
|
+
init: Any
|
|
116
|
+
spec: ResidentServiceSpec
|
|
117
|
+
lifecycle: ResidentLifecycle
|
|
118
|
+
scope: ServiceScope
|
|
119
|
+
idle_timeout: float
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def bundled_launch_config_text() -> str:
|
|
123
|
+
return resources.files("paglets.config.defaults").joinpath("launch.toml").read_text(encoding="utf-8")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def load_launch_config(path: Path | str = DEFAULT_LAUNCH_CONFIG_PATH) -> LaunchConfig:
|
|
127
|
+
config_path = Path(path).expanduser()
|
|
128
|
+
if not config_path.exists():
|
|
129
|
+
return LaunchConfig(path=config_path)
|
|
130
|
+
payload = _load_toml(config_path.read_text(encoding="utf-8"), config_path)
|
|
131
|
+
return _launch_config_from_payload(payload, config_path)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def sync_launch_config(
|
|
135
|
+
path: Path | str = DEFAULT_LAUNCH_CONFIG_PATH,
|
|
136
|
+
*,
|
|
137
|
+
enabled: bool = True,
|
|
138
|
+
yes: bool = False,
|
|
139
|
+
interactive: bool | None = None,
|
|
140
|
+
input_func: Callable[[str], str] = input,
|
|
141
|
+
output: TextIO | None = None,
|
|
142
|
+
) -> LaunchConfigSyncResult:
|
|
143
|
+
config_path = Path(path).expanduser()
|
|
144
|
+
out = output or sys.stderr
|
|
145
|
+
bundled_text = bundled_launch_config_text()
|
|
146
|
+
bundled_payload = _load_toml(bundled_text, None)
|
|
147
|
+
bundled_launch = _launch_table(bundled_payload)
|
|
148
|
+
bundled_id = str(bundled_launch.get("demo_config_id") or DEFAULT_DEMO_CONFIG_ID)
|
|
149
|
+
bundled_version = str(bundled_launch.get("demo_config_version") or "")
|
|
150
|
+
|
|
151
|
+
if not enabled:
|
|
152
|
+
return LaunchConfigSyncResult(LaunchConfigSyncAction.SKIPPED, config_path, "launch config sync disabled")
|
|
153
|
+
|
|
154
|
+
if not config_path.exists():
|
|
155
|
+
_write_launch_config(config_path, bundled_text)
|
|
156
|
+
return LaunchConfigSyncResult(
|
|
157
|
+
LaunchConfigSyncAction.COPIED,
|
|
158
|
+
config_path,
|
|
159
|
+
f"copied bundled launch config to {config_path}",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
current_text = config_path.read_text(encoding="utf-8")
|
|
163
|
+
current_payload = _load_toml(current_text, config_path)
|
|
164
|
+
current_launch = _launch_table(current_payload)
|
|
165
|
+
if not bool(current_launch.get("sync_demo_config", True)):
|
|
166
|
+
return LaunchConfigSyncResult(
|
|
167
|
+
LaunchConfigSyncAction.SKIPPED,
|
|
168
|
+
config_path,
|
|
169
|
+
"launch config disables bundled demo sync",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
current_id = str(current_launch.get("demo_config_id") or "")
|
|
173
|
+
current_version = str(current_launch.get("demo_config_version") or "")
|
|
174
|
+
if current_id == bundled_id and current_version == bundled_version:
|
|
175
|
+
return LaunchConfigSyncResult(LaunchConfigSyncAction.UNCHANGED, config_path, "launch config is up to date")
|
|
176
|
+
|
|
177
|
+
if yes:
|
|
178
|
+
backup_path = _replace_launch_config(config_path, bundled_text)
|
|
179
|
+
return LaunchConfigSyncResult(
|
|
180
|
+
LaunchConfigSyncAction.UPDATED,
|
|
181
|
+
config_path,
|
|
182
|
+
f"updated launch config from bundled demo at {config_path}",
|
|
183
|
+
backup_path,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if interactive is None:
|
|
187
|
+
interactive = sys.stdin.isatty()
|
|
188
|
+
if not interactive:
|
|
189
|
+
update_hint = (
|
|
190
|
+
f"keeping existing {config_path}. Run paglets-host with --yes to update "
|
|
191
|
+
"or --no-sync-launch-config to suppress."
|
|
192
|
+
)
|
|
193
|
+
print(
|
|
194
|
+
f"paglets host warning: bundled launch config {bundled_id} version {bundled_version} is available; "
|
|
195
|
+
f"{update_hint}",
|
|
196
|
+
file=out,
|
|
197
|
+
flush=True,
|
|
198
|
+
)
|
|
199
|
+
return LaunchConfigSyncResult(
|
|
200
|
+
LaunchConfigSyncAction.UPDATE_AVAILABLE,
|
|
201
|
+
config_path,
|
|
202
|
+
"bundled launch config update available",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
answer = (
|
|
206
|
+
input_func(
|
|
207
|
+
f"Bundled paglets launch config {bundled_id} version {bundled_version} is available. "
|
|
208
|
+
f"Replace {config_path} and move the old file aside? [y/N] "
|
|
209
|
+
)
|
|
210
|
+
.strip()
|
|
211
|
+
.lower()
|
|
212
|
+
)
|
|
213
|
+
if answer not in {"y", "yes"}:
|
|
214
|
+
return LaunchConfigSyncResult(LaunchConfigSyncAction.SKIPPED, config_path, "launch config update declined")
|
|
215
|
+
|
|
216
|
+
backup_path = _replace_launch_config(config_path, bundled_text)
|
|
217
|
+
return LaunchConfigSyncResult(
|
|
218
|
+
LaunchConfigSyncAction.UPDATED,
|
|
219
|
+
config_path,
|
|
220
|
+
f"updated launch config from bundled demo at {config_path}",
|
|
221
|
+
backup_path,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def resolve_startup_agent(config: StartupAgentConfig) -> ResolvedStartupAgent:
|
|
226
|
+
from paglets.core.agent import Paglet
|
|
227
|
+
|
|
228
|
+
agent_cls: type[Paglet[Any]]
|
|
229
|
+
spec: AutoStartSpec | None = None
|
|
230
|
+
if config.use:
|
|
231
|
+
raise HostError(f"Unknown startup agent alias {config.use!r}; use 'class' for importable paglet classes")
|
|
232
|
+
elif config.class_name:
|
|
233
|
+
resolved = resolve_qualified_name(config.class_name)
|
|
234
|
+
if not isinstance(resolved, type) or not issubclass(resolved, Paglet):
|
|
235
|
+
raise HostError(f"{config.class_name!r} is not a Paglet class")
|
|
236
|
+
agent_cls = resolved
|
|
237
|
+
maybe_spec = getattr(agent_cls, "AUTO_START", None)
|
|
238
|
+
spec = maybe_spec if isinstance(maybe_spec, AutoStartSpec) else None
|
|
239
|
+
else:
|
|
240
|
+
raise HostError("startup agent entry must set 'use' or 'class'")
|
|
241
|
+
|
|
242
|
+
state_payload: dict[str, Any] = {}
|
|
243
|
+
if spec is not None:
|
|
244
|
+
state_payload.update(spec.state)
|
|
245
|
+
state_payload.update(config.state)
|
|
246
|
+
state_cls = agent_cls.state_class()
|
|
247
|
+
try:
|
|
248
|
+
state = dataclass_from_wire(state_cls, state_payload)
|
|
249
|
+
except SerializationError as exc:
|
|
250
|
+
raise HostError(f"Could not build state for startup agent {agent_cls.__name__}") from exc
|
|
251
|
+
agent_id = config.agent_id or (spec.agent_id if spec is not None else None)
|
|
252
|
+
return ResolvedStartupAgent(
|
|
253
|
+
agent_cls=agent_cls,
|
|
254
|
+
state=state,
|
|
255
|
+
agent_id=agent_id,
|
|
256
|
+
singleton=config.singleton,
|
|
257
|
+
init=config.init,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def resolve_resident_service(config: ResidentServiceConfig) -> ResolvedResidentService:
|
|
262
|
+
from paglets.core.agent import Paglet
|
|
263
|
+
|
|
264
|
+
if config.use:
|
|
265
|
+
raise HostError(f"Unknown resident service alias {config.use!r}; use 'class' for importable paglet classes")
|
|
266
|
+
if not config.class_name:
|
|
267
|
+
raise HostError("resident service entry must set 'class'")
|
|
268
|
+
resolved = resolve_qualified_name(config.class_name)
|
|
269
|
+
if not isinstance(resolved, type) or not issubclass(resolved, Paglet):
|
|
270
|
+
raise HostError(f"{config.class_name!r} is not a Paglet class")
|
|
271
|
+
agent_cls: type[Paglet[Any]] = resolved
|
|
272
|
+
spec = _select_resident_service_spec(agent_cls, config)
|
|
273
|
+
|
|
274
|
+
state_payload = dict(spec.state)
|
|
275
|
+
state_payload.update(config.state)
|
|
276
|
+
state_cls = agent_cls.state_class()
|
|
277
|
+
try:
|
|
278
|
+
state = dataclass_from_wire(state_cls, state_payload)
|
|
279
|
+
except SerializationError as exc:
|
|
280
|
+
raise HostError(f"Could not build state for resident service {agent_cls.__name__}") from exc
|
|
281
|
+
|
|
282
|
+
lifecycle = config.lifecycle or spec.lifecycle
|
|
283
|
+
require_enum(lifecycle, ResidentLifecycle, "lifecycle")
|
|
284
|
+
scope = config.scope or spec.scope
|
|
285
|
+
require_enum(scope, ServiceScope, "scope")
|
|
286
|
+
idle_timeout = spec.idle_timeout if config.idle_timeout is None else config.idle_timeout
|
|
287
|
+
if idle_timeout < 0:
|
|
288
|
+
raise HostError("resident service idle_timeout must be non-negative")
|
|
289
|
+
|
|
290
|
+
agent_id = config.agent_id or spec.agent_id or f"service.{spec.contract.name}"
|
|
291
|
+
if not agent_id:
|
|
292
|
+
raise HostError("resident service requires an agent_id")
|
|
293
|
+
return ResolvedResidentService(
|
|
294
|
+
agent_cls=agent_cls,
|
|
295
|
+
state=state,
|
|
296
|
+
agent_id=agent_id,
|
|
297
|
+
singleton=config.singleton and spec.singleton,
|
|
298
|
+
init=config.init,
|
|
299
|
+
spec=spec,
|
|
300
|
+
lifecycle=lifecycle,
|
|
301
|
+
scope=scope,
|
|
302
|
+
idle_timeout=float(idle_timeout),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _launch_config_from_payload(payload: dict[str, Any], path: Path) -> LaunchConfig:
|
|
307
|
+
launch = _launch_table(payload)
|
|
308
|
+
raw_agents = payload.get("startup_agents", [])
|
|
309
|
+
if not isinstance(raw_agents, list):
|
|
310
|
+
raise HostError(f"{path} startup_agents must be a list")
|
|
311
|
+
raw_resident_services = payload.get("resident_services", [])
|
|
312
|
+
if not isinstance(raw_resident_services, list):
|
|
313
|
+
raise HostError(f"{path} resident_services must be a list")
|
|
314
|
+
return LaunchConfig(
|
|
315
|
+
path=path,
|
|
316
|
+
demo_config_id=str(launch["demo_config_id"]) if launch.get("demo_config_id") is not None else None,
|
|
317
|
+
demo_config_version=str(launch["demo_config_version"])
|
|
318
|
+
if launch.get("demo_config_version") is not None
|
|
319
|
+
else None,
|
|
320
|
+
sync_demo_config=bool(launch.get("sync_demo_config", True)),
|
|
321
|
+
startup_agents=tuple(_startup_agent_from_payload(item, path) for item in raw_agents),
|
|
322
|
+
resident_services=tuple(_resident_service_from_payload(item, path) for item in raw_resident_services),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _startup_agent_from_payload(payload: Any, path: Path) -> StartupAgentConfig:
|
|
327
|
+
if not isinstance(payload, dict):
|
|
328
|
+
raise HostError(f"{path} startup_agents entries must be tables")
|
|
329
|
+
state = payload.get("state", {})
|
|
330
|
+
if not isinstance(state, dict):
|
|
331
|
+
raise HostError(f"{path} startup agent state must be an inline table or table")
|
|
332
|
+
class_name = payload.get("class")
|
|
333
|
+
return StartupAgentConfig(
|
|
334
|
+
use=str(payload["use"]) if payload.get("use") is not None else None,
|
|
335
|
+
class_name=str(class_name) if class_name is not None else None,
|
|
336
|
+
enabled=bool(payload.get("enabled", True)),
|
|
337
|
+
agent_id=str(payload["agent_id"]) if payload.get("agent_id") is not None else None,
|
|
338
|
+
singleton=bool(payload.get("singleton", True)),
|
|
339
|
+
state=dict(state),
|
|
340
|
+
init=payload.get("init"),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _resident_service_from_payload(payload: Any, path: Path) -> ResidentServiceConfig:
|
|
345
|
+
if not isinstance(payload, dict):
|
|
346
|
+
raise HostError(f"{path} resident_services entries must be tables")
|
|
347
|
+
state = payload.get("state", {})
|
|
348
|
+
if not isinstance(state, dict):
|
|
349
|
+
raise HostError(f"{path} resident service state must be an inline table or table")
|
|
350
|
+
lifecycle = payload.get("lifecycle")
|
|
351
|
+
scope = payload.get("scope")
|
|
352
|
+
idle_timeout = payload.get("idle_timeout")
|
|
353
|
+
class_name = payload.get("class")
|
|
354
|
+
service_name = payload.get("service")
|
|
355
|
+
return ResidentServiceConfig(
|
|
356
|
+
use=str(payload["use"]) if payload.get("use") is not None else None,
|
|
357
|
+
class_name=str(class_name) if class_name is not None else None,
|
|
358
|
+
service_name=str(service_name) if service_name is not None else None,
|
|
359
|
+
enabled=bool(payload.get("enabled", True)),
|
|
360
|
+
agent_id=str(payload["agent_id"]) if payload.get("agent_id") is not None else None,
|
|
361
|
+
singleton=bool(payload.get("singleton", True)),
|
|
362
|
+
lifecycle=(
|
|
363
|
+
_enum_from_config(lifecycle, ResidentLifecycle, "lifecycle", path) if lifecycle is not None else None
|
|
364
|
+
),
|
|
365
|
+
scope=_enum_from_config(scope, ServiceScope, "scope", path) if scope is not None else None,
|
|
366
|
+
idle_timeout=float(idle_timeout) if idle_timeout is not None else None,
|
|
367
|
+
state=dict(state),
|
|
368
|
+
init=payload.get("init"),
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _select_resident_service_spec(agent_cls: type[Any], config: ResidentServiceConfig) -> ResidentServiceSpec:
|
|
373
|
+
raw_specs = getattr(agent_cls, "RESIDENT_SERVICES", ())
|
|
374
|
+
specs = tuple(spec for spec in raw_specs if isinstance(spec, ResidentServiceSpec))
|
|
375
|
+
if not specs:
|
|
376
|
+
raise HostError(f"{agent_cls.__name__} must declare RESIDENT_SERVICES for resident service launch config")
|
|
377
|
+
if config.service_name is not None:
|
|
378
|
+
for spec in specs:
|
|
379
|
+
if spec.contract.name == config.service_name:
|
|
380
|
+
return spec
|
|
381
|
+
raise HostError(f"{agent_cls.__name__} does not declare resident service {config.service_name!r}")
|
|
382
|
+
if len(specs) != 1:
|
|
383
|
+
raise HostError(f"{agent_cls.__name__} declares multiple resident services; set service in launch config")
|
|
384
|
+
return specs[0]
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _enum_from_config(value: Any, enum_cls: type[Any], field_name: str, path: Path) -> Any:
|
|
388
|
+
try:
|
|
389
|
+
return enum_from_wire(value, enum_cls, field_name)
|
|
390
|
+
except (TypeError, ValueError) as exc:
|
|
391
|
+
raise HostError(f"{path} resident service {field_name}: {exc}") from exc
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _load_toml(text: str, path: Path | None) -> dict[str, Any]:
|
|
395
|
+
try:
|
|
396
|
+
payload = tomllib.loads(text)
|
|
397
|
+
except tomllib.TOMLDecodeError as exc:
|
|
398
|
+
location = f"{path}: " if path is not None else ""
|
|
399
|
+
raise HostError(f"{location}invalid launch config TOML: {exc}") from exc
|
|
400
|
+
if not isinstance(payload, dict):
|
|
401
|
+
raise HostError(f"{path or 'bundled launch config'} must be a TOML table")
|
|
402
|
+
return payload
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _launch_table(payload: dict[str, Any]) -> dict[str, Any]:
|
|
406
|
+
launch = payload.get("launch", {})
|
|
407
|
+
if launch is None:
|
|
408
|
+
return {}
|
|
409
|
+
if not isinstance(launch, dict):
|
|
410
|
+
raise HostError("launch config [launch] must be a table")
|
|
411
|
+
return launch
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _write_launch_config(path: Path, text: str) -> None:
|
|
415
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
416
|
+
path.write_text(text, encoding="utf-8")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _replace_launch_config(path: Path, text: str) -> Path:
|
|
420
|
+
backup_path = _backup_path(path)
|
|
421
|
+
path.replace(backup_path)
|
|
422
|
+
_write_launch_config(path, text)
|
|
423
|
+
return backup_path
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _backup_path(path: Path) -> Path:
|
|
427
|
+
candidate = path.with_name(f"{path.name}.old")
|
|
428
|
+
if not candidate.exists():
|
|
429
|
+
return candidate
|
|
430
|
+
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
431
|
+
return path.with_name(f"{path.name}.old-{stamp}")
|
paglets/core/__init__.py
ADDED