arkitekt-runtime 7.2.0__tar.gz → 7.3.0__tar.gz

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 (63) hide show
  1. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/PKG-INFO +2 -2
  2. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/agents/base.py +30 -1
  3. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/agents/hooks/materialize.py +9 -2
  4. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/agents/hooks/startup.py +79 -2
  5. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/pyproject.toml +1 -1
  6. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/.gitignore +0 -0
  7. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/README.md +0 -0
  8. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/__init__.py +0 -0
  9. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/actors/__init__.py +0 -0
  10. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/actors/actify.py +0 -0
  11. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/actors/base.py +0 -0
  12. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/actors/build.py +0 -0
  13. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/actors/debug.py +0 -0
  14. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/actors/dependency.py +0 -0
  15. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/actors/errors.py +0 -0
  16. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/actors/functional.py +0 -0
  17. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/actors/helper.py +0 -0
  18. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/actors/interactions.py +0 -0
  19. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/actors/types.py +0 -0
  20. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/agents/__init__.py +0 -0
  21. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/agents/backend.py +0 -0
  22. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/agents/dataclasses.py +0 -0
  23. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/agents/hardware.py +0 -0
  24. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/agents/hooks/__init__.py +0 -0
  25. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/agents/hooks/background.py +0 -0
  26. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/agents/hooks/shutdown.py +0 -0
  27. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/agents/journal.py +0 -0
  28. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/agents/lock.py +0 -0
  29. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/agents/policy.py +0 -0
  30. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/agents/transport/__init__.py +0 -0
  31. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/agents/transport/base.py +0 -0
  32. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/agents/transport/types.py +0 -0
  33. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/calls.py +0 -0
  34. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/constants.py +0 -0
  35. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/invoke.py +0 -0
  36. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/local.py +0 -0
  37. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/messages.py +0 -0
  38. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/postmans/__init__.py +0 -0
  39. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/postmans/errors.py +0 -0
  40. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/postmans/types.py +0 -0
  41. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/qt/__init__.py +0 -0
  42. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/qt/builders.py +0 -0
  43. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/state/__init__.py +0 -0
  44. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/state/expand.py +0 -0
  45. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/state/gate.py +0 -0
  46. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/state/observable.py +0 -0
  47. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/state/readonly.py +0 -0
  48. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/state/shrink.py +0 -0
  49. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/state/write.py +0 -0
  50. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/structures/__init__.py +0 -0
  51. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/structures/serialization/__init__.py +0 -0
  52. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/structures/serialization/actor.py +0 -0
  53. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/structures/serialization/batching.py +0 -0
  54. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/structures/serialization/context.py +0 -0
  55. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/structures/serialization/expand.py +0 -0
  56. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/structures/serialization/memory.py +0 -0
  57. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/structures/serialization/port_errors.py +0 -0
  58. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/structures/serialization/postman.py +0 -0
  59. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/structures/serialization/predication.py +0 -0
  60. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/structures/serialization/shrink.py +0 -0
  61. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/task.py +0 -0
  62. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/task_scope.py +0 -0
  63. {arkitekt_runtime-7.2.0 → arkitekt_runtime-7.3.0}/arkitekt_runtime/types.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: arkitekt-runtime
3
- Version: 7.2.0
3
+ Version: 7.3.0
4
4
  Summary: the execution core of an Arkitekt app: agent, actors, tasks, journal, state and structure serialization, shared by every runtime
5
5
  Author-email: jhnnsrs <jhnnsrs@gmail.com>
6
6
  License-Expression: MIT
7
7
  Requires-Python: >=3.11
8
- Requires-Dist: arkitekt-spec>=6.2
8
+ Requires-Dist: arkitekt-spec>=6.3.1
9
9
  Requires-Dist: janus>=2.0.0
10
10
  Requires-Dist: jsonpatch>=1.33
11
11
  Requires-Dist: koil>=3.3.6
@@ -318,6 +318,8 @@ class BaseAgent(KoiledModel):
318
318
  default_factory=lambda: {}
319
319
  )
320
320
  _ran_startup_hooks: bool = PrivateAttr(default=False)
321
+ _entered_lifecycles: list[Any] = PrivateAttr(default_factory=list)
322
+ """The lifecycle hooks started in this run, in order: what follows their yield is owed."""
321
323
  # A stopped run reaches teardown from several places (the loop, leaving the
322
324
  # agent); it runs once per connect.
323
325
  _torn_down: bool = PrivateAttr(default=False)
@@ -1824,6 +1826,8 @@ class BaseAgent(KoiledModel):
1824
1826
  for key, hook in self._collected_startup_hooks.items():
1825
1827
  try:
1826
1828
  previous = await self._aprevious_state_kwargs(hook)
1829
+ if hasattr(hook, "afinish"):
1830
+ self._entered_lifecycles.append(hook)
1827
1831
  answer = await asyncio.wait_for(
1828
1832
  hook.arun(
1829
1833
  app_context=app_context,
@@ -1887,6 +1891,25 @@ class BaseAgent(KoiledModel):
1887
1891
  hook_error.__cause__ = e
1888
1892
  logger.error(hook_error, exc_info=hook_error)
1889
1893
 
1894
+ # After the shutdown hooks: they may take what these set up.
1895
+ await self._afinish_lifecycles()
1896
+
1897
+ async def _afinish_lifecycles(self) -> None:
1898
+ """Run what follows each lifecycle hook's yield, the last one entered first.
1899
+
1900
+ Like a shutdown hook, one that fails or times out is logged, and the rest still run.
1901
+ """
1902
+ from arkitekt_spec.declare.agents.hooks.errors import ShutdownHookError
1903
+
1904
+ entered, self._entered_lifecycles = self._entered_lifecycles, []
1905
+ for hook in reversed(entered):
1906
+ try:
1907
+ await asyncio.wait_for(hook.afinish(), timeout=self.shutdown_hook_timeout)
1908
+ except Exception as e:
1909
+ hook_error = ShutdownHookError(f"Lifecycle hook {hook.func.__name__} failed")
1910
+ hook_error.__cause__ = e
1911
+ logger.error(hook_error, exc_info=hook_error)
1912
+
1890
1913
  @property
1891
1914
  def registered_agent_id(self) -> str | None:
1892
1915
  """The id this agent's backend assigned it, once registered.
@@ -1928,7 +1951,13 @@ class BaseAgent(KoiledModel):
1928
1951
  """
1929
1952
  # A state is built unrestricted by its startup hook; adopting it (in
1930
1953
  # ainit_states) is what makes later changes need their locks.
1931
- hook_return = await self.arun_startup_hooks(app_context=app_context)
1954
+ try:
1955
+ hook_return = await self.arun_startup_hooks(app_context=app_context)
1956
+ except BaseException:
1957
+ # No shutdown hook runs for a start that failed here, but the lifecycle
1958
+ # hooks before the failing one are entered.
1959
+ await self._afinish_lifecycles()
1960
+ raise
1932
1961
  # From here on the app has set up resources, so teardown owes it the
1933
1962
  # shutdown hooks (even if the rest of the startup fails).
1934
1963
  self._ran_startup_hooks = True
@@ -4,6 +4,7 @@ from typing import Any
4
4
 
5
5
  from arkitekt_spec.declare.agents.hooks.declare import (
6
6
  BackgroundDeclaration,
7
+ LifecycleHookDeclaration,
7
8
  ShutdownHookDeclaration,
8
9
  StartupHookDeclaration,
9
10
  )
@@ -12,7 +13,11 @@ from arkitekt_runtime.agents.hooks.background import (
12
13
  WrappedThreadedBackgroundTask,
13
14
  )
14
15
  from arkitekt_runtime.agents.hooks.shutdown import ThreadedShutdownHook, WrappedShutdownHook
15
- from arkitekt_runtime.agents.hooks.startup import ThreadedStartupHook, WrappedStartupHook
16
+ from arkitekt_runtime.agents.hooks.startup import (
17
+ ThreadedStartupHook,
18
+ WrappedLifecycleHook,
19
+ WrappedStartupHook,
20
+ )
16
21
 
17
22
 
18
23
  def materialize_hook(declaration: Any) -> Any: # noqa: ANN401
@@ -24,7 +29,9 @@ def materialize_hook(declaration: Any) -> Any: # noqa: ANN401
24
29
  if hasattr(declaration, "arun"):
25
30
  return declaration
26
31
  func, registry = declaration.func, declaration.structure_registry
27
- if isinstance(declaration, StartupHookDeclaration):
32
+ if isinstance(declaration, LifecycleHookDeclaration):
33
+ cls = WrappedLifecycleHook
34
+ elif isinstance(declaration, StartupHookDeclaration):
28
35
  cls = ThreadedStartupHook if declaration.threaded else WrappedStartupHook
29
36
  elif isinstance(declaration, BackgroundDeclaration):
30
37
  cls = WrappedThreadedBackgroundTask if declaration.threaded else WrappedBackgroundTask
@@ -1,16 +1,20 @@
1
1
  """Hooks for the agent"""
2
2
 
3
+ import asyncio
3
4
  from typing import (
4
5
  TYPE_CHECKING,
5
6
  Any,
6
7
  )
7
8
  from arkitekt_spec.declare.agents.types import BoundApp
8
9
  from koil.bridge import run_threaded
9
- from arkitekt_spec.declare.agents.hooks.errors import StartupHookError
10
+ from arkitekt_spec.declare.agents.hooks.errors import ShutdownHookError, StartupHookError
10
11
  from arkitekt_spec.declare.agents.hooks.registry import (
11
12
  StartupHookReturns,
12
13
  )
13
- from arkitekt_spec.declare.agents.hooks.declare import StartupWithVariables
14
+ from arkitekt_spec.declare.agents.hooks.declare import (
15
+ LifecycleWithVariables,
16
+ StartupWithVariables,
17
+ )
14
18
 
15
19
  if TYPE_CHECKING:
16
20
  from arkitekt_spec.declare.structures.registry import StructureRegistry
@@ -118,4 +122,77 @@ class ThreadedStartupHook(StartupWithVariables):
118
122
  return _split_returns(self, ensure_return_as_tuple(parsed_returns))
119
123
 
120
124
 
125
+ class WrappedLifecycleHook(LifecycleWithVariables):
126
+ """A lifecycle hook, driven by a task of its own.
127
+
128
+ The agent starts and tears down in different tasks, and what an async generator enters
129
+ (a cancel scope, ``asyncio.timeout``) must be left in the task that entered it.
130
+ """
131
+
132
+ def __init__(
133
+ self, func: Any, structure_registry: "StructureRegistry | None" = None # noqa: ANN401
134
+ ) -> None:
135
+ super().__init__(func, structure_registry)
136
+ self._task: asyncio.Task[None] | None = None
137
+ self._stop = asyncio.Event()
138
+
139
+ async def arun(
140
+ self,
141
+ app_context: Any, # noqa: ANN401
142
+ bound_app: BoundApp | None = None,
143
+ previous_states: dict[str, Any] | None = None,
144
+ ) -> StartupHookReturns:
145
+ """Run the hook up to its yield, in its own task."""
146
+ kwargs = self.get_startup_kwargs(app_context, bound_app, previous_states)
147
+ entered: asyncio.Future[Any] = asyncio.get_running_loop().create_future()
148
+ self._stop = asyncio.Event()
149
+ self._task = asyncio.create_task(self._adrive(kwargs, entered))
150
+ try:
151
+ yielded = await entered
152
+ except BaseException:
153
+ # Its setup failed, or ran out of time: there is nothing left to finish.
154
+ task, self._task = self._task, None
155
+ task.cancel()
156
+ await asyncio.gather(task, return_exceptions=True)
157
+ raise
158
+ return _split_returns(self, ensure_return_as_tuple(yielded))
159
+
160
+ async def afinish(self) -> None:
161
+ """Run what follows the yield. Nothing if the hook is not entered."""
162
+ task, self._task = self._task, None
163
+ if task is None:
164
+ return
165
+ self._stop.set()
166
+ await task
167
+
168
+ async def _adrive(self, kwargs: dict[str, Any], entered: "asyncio.Future[Any]") -> None:
169
+ name = self.func.__name__
170
+ generator = self.func(**kwargs)
171
+ try:
172
+ try:
173
+ yielded = await anext(generator)
174
+ except StopAsyncIteration:
175
+ if not entered.done():
176
+ entered.set_exception(StartupHookError(f"Lifecycle hook {name} did not yield"))
177
+ return
178
+ except asyncio.CancelledError:
179
+ entered.cancel()
180
+ raise
181
+ except Exception as e:
182
+ if not entered.done():
183
+ entered.set_exception(e)
184
+ return
185
+ if entered.done():
186
+ return
187
+ entered.set_result(yielded)
188
+ await self._stop.wait()
189
+ try:
190
+ await anext(generator)
191
+ except StopAsyncIteration:
192
+ return
193
+ raise ShutdownHookError(f"Lifecycle hook {name} yielded more than once")
194
+ finally:
195
+ await generator.aclose()
196
+
197
+
121
198
  # --- Implementation ---
@@ -10,7 +10,7 @@ license = "MIT"
10
10
  # server) executes actions the same way, so this is what they share. It carries no
11
11
  # transport and no client: no websockets, no rath, no graphql-core.
12
12
  dependencies = [
13
- "arkitekt-spec>=6.2",
13
+ "arkitekt-spec>=6.3.1",
14
14
  "pydantic>=2.11",
15
15
  "koil>=3.3.6",
16
16
  "janus>=2.0.0",