arkitekt-runtime 4.2.0__tar.gz → 4.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 (60) hide show
  1. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/PKG-INFO +3 -3
  2. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/actors/types.py +1 -1
  3. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/agents/base.py +61 -2
  4. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/state/observable.py +0 -4
  5. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/structures/serialization/actor.py +9 -0
  6. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/structures/serialization/shrink.py +6 -0
  7. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/task.py +5 -1
  8. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/pyproject.toml +2 -2
  9. arkitekt_runtime-4.2.0/arkitekt_runtime/agents/connection.py +0 -19
  10. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/.gitignore +0 -0
  11. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/README.md +0 -0
  12. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/__init__.py +0 -0
  13. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/actors/__init__.py +0 -0
  14. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/actors/actify.py +0 -0
  15. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/actors/base.py +0 -0
  16. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/actors/build.py +0 -0
  17. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/actors/debug.py +0 -0
  18. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/actors/dependency.py +0 -0
  19. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/actors/errors.py +0 -0
  20. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/actors/functional.py +0 -0
  21. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/actors/helper.py +0 -0
  22. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/agents/__init__.py +0 -0
  23. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/agents/backend.py +0 -0
  24. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/agents/dataclasses.py +0 -0
  25. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/agents/hooks/__init__.py +0 -0
  26. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/agents/hooks/background.py +0 -0
  27. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/agents/hooks/materialize.py +0 -0
  28. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/agents/hooks/shutdown.py +0 -0
  29. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/agents/hooks/startup.py +0 -0
  30. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/agents/journal.py +0 -0
  31. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/agents/lock.py +0 -0
  32. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/agents/policy.py +0 -0
  33. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/agents/transport/__init__.py +0 -0
  34. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/agents/transport/base.py +0 -0
  35. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/agents/transport/types.py +0 -0
  36. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/calls.py +0 -0
  37. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/constants.py +0 -0
  38. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/invoke.py +0 -0
  39. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/messages.py +0 -0
  40. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/postmans/__init__.py +0 -0
  41. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/postmans/errors.py +0 -0
  42. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/postmans/types.py +0 -0
  43. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/qt/__init__.py +0 -0
  44. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/qt/builders.py +0 -0
  45. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/state/__init__.py +0 -0
  46. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/state/gate.py +0 -0
  47. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/state/readonly.py +0 -0
  48. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/state/shrink.py +0 -0
  49. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/state/write.py +0 -0
  50. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/structures/__init__.py +0 -0
  51. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/structures/serialization/__init__.py +0 -0
  52. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/structures/serialization/batching.py +0 -0
  53. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/structures/serialization/context.py +0 -0
  54. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/structures/serialization/expand.py +0 -0
  55. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/structures/serialization/memory.py +0 -0
  56. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/structures/serialization/port_errors.py +0 -0
  57. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/structures/serialization/postman.py +0 -0
  58. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/structures/serialization/predication.py +0 -0
  59. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/task_scope.py +0 -0
  60. {arkitekt_runtime-4.2.0 → arkitekt_runtime-4.3.0}/arkitekt_runtime/types.py +0 -0
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: arkitekt-runtime
3
- Version: 4.2.0
3
+ Version: 4.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>=2.0.1
8
+ Requires-Dist: arkitekt-spec>=3.4
9
9
  Requires-Dist: janus>=2.0.0
10
10
  Requires-Dist: jsonpatch>=1.33
11
- Requires-Dist: koil>=3.3.3
11
+ Requires-Dist: koil>=3.3.6
12
12
  Requires-Dist: pydantic>=2.11
13
13
  Provides-Extra: qt
14
14
  Requires-Dist: qtpy>=2.4; extra == 'qt'
@@ -22,7 +22,7 @@ from collections.abc import Sequence
22
22
 
23
23
  if TYPE_CHECKING:
24
24
  from arkitekt_spec.declare.app import AppRegistry
25
- from arkitekt_runtime.agents.connection import ConnectionListener
25
+ from arkitekt_spec.declare.agents.connection import ConnectionListener
26
26
  from arkitekt_runtime.agents.lock import TaskLock
27
27
 
28
28
 
@@ -56,7 +56,13 @@ from arkitekt_runtime.agents.journal import (
56
56
  )
57
57
  from arkitekt_runtime.task_scope import current_task_id, task_id_scope
58
58
  from arkitekt_spec.declare.agents.types import AppContext, T
59
- from arkitekt_runtime.agents.connection import ConnectionListener, ConnectionState
59
+ from arkitekt_spec.declare.agents.connection import (
60
+ ConnectionListener,
61
+ ConnectionState,
62
+ TaskEvent,
63
+ TaskEventKind,
64
+ TaskListener,
65
+ )
60
66
  from arkitekt_runtime.agents.policy import ConnectionPolicy
61
67
  from arkitekt_spec.declare.agents.hooks.registry import (
62
68
  ShutdownHook,
@@ -220,7 +226,13 @@ class BaseAgent(KoiledModel):
220
226
  connection_listener: ConnectionListener | None = Field(
221
227
  default=None,
222
228
  exclude=True,
223
- description="Told when the backend acknowledges the agent and when the link drops (see arkitekt_runtime.agents.connection). Settable per run, like force; None reports to nobody.",
229
+ description="Told when the backend acknowledges the agent and when the link drops (see arkitekt_spec.declare.agents.connection). Settable per run, like force; None reports to nobody.",
230
+ )
231
+
232
+ task_listener: TaskListener | None = Field(
233
+ default=None,
234
+ exclude=True,
235
+ description="Told what happens to each task this agent runs: assigned (with its arguments), progress, yielded, done, failed, cancelled. Settable per run; None reports to nobody.",
224
236
  )
225
237
 
226
238
  contexts: dict[str, Any] = Field(
@@ -968,6 +980,14 @@ class BaseAgent(KoiledModel):
968
980
  # From here on the task can end, and after its end nothing more is recorded
969
981
  # for it: its reports and state changes enter this gate.
970
982
  self._task_gates[message.task] = TaskGate()
983
+ await self._anotify_task(
984
+ TaskEvent(
985
+ task_id=message.task,
986
+ action=message.interface,
987
+ kind=TaskEventKind.ASSIGNED,
988
+ arguments=dict(message.args),
989
+ )
990
+ )
971
991
  # ...and it is this process's: numbered by step (unless it is a probe).
972
992
  self._journal.begin_task(message.task)
973
993
  if message.resume is not None:
@@ -1206,6 +1226,41 @@ class BaseAgent(KoiledModel):
1206
1226
  except Exception:
1207
1227
  logger.warning("The connection listener failed on %s", state.value, exc_info=True)
1208
1228
 
1229
+ def _task_event_of(self, message: messages.FromAgentMessage) -> TaskEvent | None:
1230
+ """What a task's report means to a task listener; ``None`` for anything else."""
1231
+ if isinstance(message, messages.Progress):
1232
+ kind, details = TaskEventKind.PROGRESS, {
1233
+ "progress": message.progress,
1234
+ "message": message.message,
1235
+ }
1236
+ elif isinstance(message, messages.Yield):
1237
+ kind, details = TaskEventKind.YIELDED, {}
1238
+ elif isinstance(message, messages.Completed):
1239
+ kind, details = TaskEventKind.DONE, {}
1240
+ elif isinstance(message, (messages.Failed, messages.Critical)):
1241
+ kind, details = TaskEventKind.FAILED, {"error": message.error}
1242
+ elif isinstance(message, (messages.Cancelled, messages.Interrupted)):
1243
+ kind, details = TaskEventKind.CANCELLED, {}
1244
+ else:
1245
+ return None
1246
+ return TaskEvent(
1247
+ task_id=message.task,
1248
+ action=self._action_key_for(message) or message.task,
1249
+ kind=kind,
1250
+ **details,
1251
+ )
1252
+
1253
+ async def _anotify_task(self, event: TaskEvent) -> None:
1254
+ """Tell the task listener, if there is one."""
1255
+ if self.task_listener is None:
1256
+ return
1257
+ try:
1258
+ await self.task_listener(event)
1259
+ except Exception:
1260
+ logger.warning(
1261
+ "The task listener failed on %s of %s", event.kind.value, event.task_id, exc_info=True
1262
+ )
1263
+
1209
1264
  def _start_disconnect_watchdog(self) -> None:
1210
1265
  """Begin counting down the grace periods of disconnect-sensitive actors."""
1211
1266
  if (
@@ -1400,6 +1455,10 @@ class BaseAgent(KoiledModel):
1400
1455
  )
1401
1456
  self._retain_emitted(message, entry)
1402
1457
  await self._adeliver(message, entry)
1458
+ if self.task_listener is not None:
1459
+ event = self._task_event_of(message)
1460
+ if event is not None:
1461
+ await self._anotify_task(event)
1403
1462
  if isinstance(message, messages.TERMINAL_REPORTS):
1404
1463
  # The task is done, so it is no longer running on any actor, and no
1405
1464
  # longer an assignment this agent is managing. Nothing used to pop
@@ -70,9 +70,6 @@ class StateConfig:
70
70
 
71
71
  state_name: str
72
72
  definition: StateDefinitionInput
73
- publish_interval: float = (
74
- 0.1 # Optional: Minimum interval between patches to prevent flooding
75
- )
76
73
  required_locks: list[str] = dataclasses.field(default_factory=list)
77
74
  publisher: StateHolder | None = dataclasses.field(default=None, compare=False)
78
75
  """Where patches go: the agent that adopted the state. None until then."""
@@ -91,7 +88,6 @@ class StateConfig:
91
88
  return cls(
92
89
  state_name=declaration.interface,
93
90
  definition=declaration.definition,
94
- publish_interval=declaration.publish_interval,
95
91
  required_locks=list(declaration.required_locks),
96
92
  )
97
93
 
@@ -660,6 +660,15 @@ async def _shrink_structure(
660
660
  "Port is structure but does not have identifier. Please report this to the developers.",
661
661
  )
662
662
  fstruc = ctx.registry.get_fullfilled_structure(port.identifier)
663
+ other = ctx.registry.declared_as_another(value, port.identifier)
664
+ if other is not None:
665
+ raise _shrink_error(
666
+ port,
667
+ value,
668
+ ctx,
669
+ f"A {type(value).__name__} ({other.identifier}) was returned where "
670
+ f"{port.identifier} is declared; its id would be sent as the wrong structure.",
671
+ )
663
672
  try:
664
673
  shrunk = await fstruc.shrink(value)
665
674
  except Exception as e:
@@ -166,6 +166,12 @@ async def _structure(
166
166
  # A bare string is taken as a reference to a global structure.
167
167
  return value
168
168
  fstruc = ctx.registry.get_fullfilled_structure(port.identifier)
169
+ other = ctx.registry.declared_as_another(value, port.identifier)
170
+ if other is not None:
171
+ raise StructureShrinkingError(
172
+ f"A {type(value).__name__} ({other.identifier}) was given where "
173
+ f"{port.identifier} is declared; its id would be sent as the wrong structure."
174
+ )
169
175
  try:
170
176
  shrunk = await fstruc.shrink(value)
171
177
  except Exception:
@@ -29,7 +29,7 @@ import secrets
29
29
  import time
30
30
  from typing import TYPE_CHECKING, Any, ClassVar
31
31
 
32
- from koil import unkoil, unkoil_gen
32
+ from koil import check_cancelled, unkoil, unkoil_gen
33
33
 
34
34
  from arkitekt_spec.actions import Execution
35
35
  from arkitekt_spec.declare.agents.errors import NoCallerError
@@ -141,6 +141,10 @@ class Task:
141
141
  """Pause here if the task was asked to."""
142
142
  unkoil(self.apausepoint)
143
143
 
144
+ def check_cancelled(self) -> None:
145
+ """Stop here if the task was cancelled: the cancellation point of a sync action."""
146
+ check_cancelled()
147
+
144
148
  # -- effects ---------------------------------------------------------- #
145
149
  # Values the task takes from outside itself, each recorded as an ``EFFECT`` under a
146
150
  # key. A resumed workflow gets the recorded value back instead of taking a new one.
@@ -10,9 +10,9 @@ 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>=2.0.1",
13
+ "arkitekt-spec>=3.4",
14
14
  "pydantic>=2.11",
15
- "koil>=3.3.3",
15
+ "koil>=3.3.6",
16
16
  "janus>=2.0.0",
17
17
  "jsonpatch>=1.33",
18
18
  ]
@@ -1,19 +0,0 @@
1
- """What an agent tells whoever runs it about its connection to the backend."""
2
-
3
- from collections.abc import Awaitable, Callable
4
- from enum import Enum
5
-
6
-
7
- class ConnectionState(str, Enum):
8
- """Where the agent's connection to the backend stands."""
9
-
10
- REGISTERED = "registered"
11
- """The backend acknowledged the agent's registration (an ``Init``). Reported on
12
- the first connection and again whenever a dropped one is back."""
13
- DISCONNECTED = "disconnected"
14
- """The link dropped and the transport is trying to get it back."""
15
-
16
-
17
- ConnectionListener = Callable[[ConnectionState], Awaitable[None]]
18
- """Called with each change of the agent's connection. It only reports: what it
19
- raises is logged and dropped, never the agent's problem."""