harness-sdk-python 0.13.1__tar.gz → 0.13.2__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 (31) hide show
  1. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/PKG-INFO +1 -1
  2. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/pyproject.toml +1 -1
  3. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/src/harness_sdk/run_manager.py +15 -26
  4. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_batches.py +5 -11
  5. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_stopping_window.py +15 -17
  6. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/.gitignore +0 -0
  7. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/README.md +0 -0
  8. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/examples/__init__.py +0 -0
  9. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/examples/runs_app.py +0 -0
  10. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/src/harness_sdk/__init__.py +0 -0
  11. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/src/harness_sdk/fenced_postgres.py +0 -0
  12. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/src/harness_sdk/linear_thread.py +0 -0
  13. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/run_helpers.py +0 -0
  14. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_ack_visibility.py +0 -0
  15. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_branch_anchor.py +0 -0
  16. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_dispatching.py +0 -0
  17. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_edit_dispatched.py +0 -0
  18. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_edit_reload.py +0 -0
  19. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_enqueue.py +0 -0
  20. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_fenced_postgres.py +0 -0
  21. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_input_required.py +0 -0
  22. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_linear_thread.py +0 -0
  23. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_meta.py +0 -0
  24. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_outcomes.py +0 -0
  25. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_placement.py +0 -0
  26. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_prepare_hooks.py +0 -0
  27. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_restore.py +0 -0
  28. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_rewind_during_run.py +0 -0
  29. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_settle.py +0 -0
  30. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_steer.py +0 -0
  31. {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.2}/tests/test_stop_continue.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: harness-sdk-python
3
- Version: 0.13.1
3
+ Version: 0.13.2
4
4
  Summary: RunManager: the harness-sdk runs subsystem for Python Statewire hosts
5
5
  Project-URL: Repository, https://github.com/assistant-ui/harness-sdk
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "harness-sdk-python"
3
- version = "0.13.1"
3
+ version = "0.13.2"
4
4
  description = "RunManager: the harness-sdk runs subsystem for Python Statewire hosts"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -10,10 +10,10 @@ dispatchable send appends straight into ``dispatching`` — it never transits a
10
10
  queue lane — and everything staged there by the end of the drain dispatches
11
11
  as one run; in error/stop the dispatch is the continuation
12
12
  (``error-continue`` / ``stop-continue``) carrying the send's messages. Every
13
- initiator acks and settles accepted with its state-visible application
14
- sends with their insert (lane or ``dispatching``), edit/reload/continue at
15
- their dispatch; ``run/stop`` awaits a future the drain resolves once the
16
- in-flight run has ended.
13
+ initiator acks and settles accepted with its state-visible application:
14
+ the verdict certifies the admission write — sends with their insert (lane
15
+ or ``dispatching``), mid-run edit/reload with the stopping window,
16
+ ``run/stop`` with ``stopping`` — never future execution.
17
17
 
18
18
  ``entry["dispatching"]`` is the dispatch record (absent between an ack and
19
19
  the next dispatch or take): the payload the run callback receives.
@@ -25,8 +25,8 @@ A mid-run stop or rewind opens a stopping window on the entry:
25
25
  ``stopping: {reason}`` plus, for rewinds, the staged dispatch as
26
26
  ``nextDispatch`` — written in the same envelope as the opener's pending
27
27
  answer. On run end one envelope promotes ``nextDispatch`` into
28
- ``dispatching`` and clears the window; a later stop/rewind supersedes the
29
- staged one, which settles rejected (``superseded``).
28
+ ``dispatching`` and clears the window; a later stop/rewind replaces the
29
+ staged dispatch, whose command already settled at its admission write.
30
30
  """
31
31
 
32
32
  import asyncio
@@ -209,7 +209,7 @@ class RunManager:
209
209
  self._dispatch_record: dict[str, Any] | None = None
210
210
  self._intake: list[Any] = []
211
211
  self._outcome: Any = None
212
- self._staged_stops: list["asyncio.Future[Any]"] = []
212
+ self._stop_staged = False
213
213
  self._stop_reason: str | None = None
214
214
  self._staged_rewind: _Rewind | None = None
215
215
  self._dispatching: list[tuple[str, dict[str, Any]]] = []
@@ -354,10 +354,7 @@ class RunManager:
354
354
  for entry in entries:
355
355
  self._apply(entry, fx)
356
356
  if self._task is None:
357
- for stop in self._staged_stops:
358
- if not stop.done():
359
- stop.set_result(None)
360
- self._staged_stops.clear()
357
+ self._stop_staged = False
361
358
  if isinstance(outcome, RunManager.Complete):
362
359
  self._dispatched_ids = ()
363
360
  if self._staged_rewind is not None:
@@ -378,14 +375,12 @@ class RunManager:
378
375
  )
379
376
  if not rewind.acked:
380
377
  rewind.ack()
381
- if not rewind.future.done():
382
- rewind.future.set_result(None)
383
378
  elif outcome is not None:
384
379
  self._settle_outcome(outcome)
385
380
  else:
386
381
  self._idle_action(fx)
387
382
  if self._task is not None and (
388
- self._staged_stops or self._staged_rewind is not None
383
+ self._stop_staged or self._staged_rewind is not None
389
384
  ):
390
385
  assert self._ctx is not None
391
386
  self._ctx.stop_requested.set()
@@ -401,12 +396,14 @@ class RunManager:
401
396
 
402
397
  def _apply(self, entry: Any, fx: _Effects) -> None:
403
398
  if isinstance(entry, _Stop):
404
- self._staged_stops.append(entry.future)
405
399
  if self._task is not None:
400
+ self._stop_staged = True
406
401
  if self._staged_rewind is not None:
407
- self._supersede()
402
+ self._staged_rewind = None
408
403
  self._entry().pop("nextDispatch", None)
409
404
  self._entry()["stopping"] = {"reason": "stop"}
405
+ if not entry.future.done():
406
+ entry.future.set_result(None)
410
407
  return
411
408
  if isinstance(entry, _Continue):
412
409
  if not fx.continue_requested:
@@ -683,17 +680,7 @@ class RunManager:
683
680
  return True
684
681
  return False
685
682
 
686
- def _supersede(self) -> None:
687
- rewind, self._staged_rewind = self._staged_rewind, None
688
- assert rewind is not None
689
- if not rewind.future.done():
690
- rewind.future.set_exception(
691
- _reject("superseded", "a later stop/rewind replaced this command")
692
- )
693
-
694
683
  def _stage_rewind(self, rewind: _Rewind) -> None:
695
- if self._staged_rewind is not None:
696
- self._supersede()
697
684
  self._staged_rewind = rewind
698
685
  if self._task is not None:
699
686
  entry = self._entry()
@@ -706,6 +693,8 @@ class RunManager:
706
693
  )
707
694
  rewind.ack()
708
695
  rewind.acked = True
696
+ if not rewind.future.done():
697
+ rewind.future.set_result(None)
709
698
 
710
699
  def _freeze(self, message: str, payload: Any = None) -> None:
711
700
  self._set_status("error")
@@ -59,30 +59,24 @@ async def test_steer_and_stop_in_one_batch_nets_to_stop():
59
59
  [("run/steer", add("s1")), ("run/stop", {"runId": await drv.run_id()})]
60
60
  )
61
61
  assert (await drv.res(first))["type"] == "accepted"
62
- assert (await drv.res(first + 1, terminal=False))["type"] == "pending"
62
+ assert (await drv.res(first + 1))["type"] == "accepted"
63
63
  await asyncio.wait_for(call.ctx.stop_requested.wait(), 5)
64
64
  call.finish(RunManager.Stop(dispatch_queue=False))
65
- assert (await drv.res(first + 1))["type"] == "accepted"
66
65
  await drv.wait_status("stopped")
67
66
  assert queue_ids(drv.replica, "steerQueue") == ["s1"]
68
67
  script.no_call()
69
68
 
70
69
 
71
- async def test_stop_settles_only_after_the_run_task_ends():
70
+ async def test_stop_settles_at_the_stopping_write():
72
71
  script = Script()
73
72
  async with run_host(script) as (drv, host):
74
73
  drv.post("run/enqueue", add("m1"))
75
74
  call = await script.next_call()
76
75
  call.ack()
77
- pending = await drv.stop(terminal=False)
78
- assert pending["type"] == "pending"
79
- for _ in range(5):
80
- await asyncio.sleep(0)
81
- assert all(
82
- rsp["type"] == "pending" for rsp in drv._res.get(pending["seq"], [])
83
- )
76
+ settled = await drv.stop()
77
+ assert settled["type"] == "accepted"
78
+ assert drv.replica["status"] == "running"
84
79
  call.finish(RunManager.Stop(dispatch_queue=False))
85
- assert (await drv.res(pending["seq"]))["type"] == "accepted"
86
80
  await drv.wait_status("stopped")
87
81
 
88
82
 
@@ -1,10 +1,10 @@
1
1
  """Contract: the stopping window. Three openers — run/stop, mid-run
2
2
  edit/reload, and the enqueue/steer edit-of-dispatched form — write
3
- ``stopping: {reason}`` (plus the staged rewind as ``nextDispatch``) in the
4
- same envelope as the opener's pending answer, ``status`` staying "running".
5
- On run end one envelope promotes ``nextDispatch`` into ``dispatching`` and
6
- clears the window; a later stop/rewind supersedes the staged command, which
7
- settles rejected (``superseded``)."""
3
+ ``stopping: {reason}`` (plus the staged rewind as ``nextDispatch``) and
4
+ settle accepted with that write, ``status`` staying "running". On run end
5
+ one envelope promotes ``nextDispatch`` into ``dispatching`` and clears the
6
+ window; a later stop/rewind replaces the staged dispatch, whose command
7
+ already settled at its admission write."""
8
8
 
9
9
  import asyncio
10
10
  import copy
@@ -13,7 +13,6 @@ import pytest
13
13
  from run_helpers import (
14
14
  Script,
15
15
  add,
16
- assert_rejected,
17
16
  edit_params,
18
17
  msg,
19
18
  queue_ids,
@@ -57,23 +56,22 @@ async def start_run(drv, script, *, parent="p0"):
57
56
  return call
58
57
 
59
58
 
60
- async def test_stop_opens_the_window_with_the_pending_answer():
59
+ async def test_stop_opens_the_window_and_settles_with_its_write():
61
60
  script = Script()
62
61
  async with run_host(script) as (drv, host):
63
62
  call = await start_run(drv, script)
64
- pending = await drv.stop(terminal=False)
65
- assert pending["type"] == "pending"
63
+ settled = await drv.stop()
64
+ assert settled["type"] == "accepted"
66
65
  assert drv.replica["status"] == "running"
67
66
  assert drv.run["stopping"] == {"reason": "stop"}
68
67
  assert "nextDispatch" not in drv.run
69
68
  opened = next(
70
69
  state
71
70
  for env, state in states(drv)
72
- if any(r["seq"] == pending["seq"] for r in env.get("res", []))
71
+ if any(r["seq"] == settled["seq"] for r in env.get("res", []))
73
72
  )
74
73
  assert window_of(opened) == ({"reason": "stop"}, None)
75
74
  call.finish(RunManager.Stop(dispatch_queue=False))
76
- assert (await drv.res(pending["seq"]))["type"] == "accepted"
77
75
  await drv.wait_status("stopped")
78
76
  assert "stopping" not in drv.run
79
77
 
@@ -178,7 +176,7 @@ async def test_edit_of_dispatched_message_opens_the_window(command):
178
176
  rerun.finish(RunManager.Complete())
179
177
 
180
178
 
181
- async def test_later_rewind_supersedes_the_staged_one():
179
+ async def test_later_rewind_replaces_the_staged_dispatch():
182
180
  script = Script()
183
181
  async with run_host(script, capabilities=("rewind", "rewind-during-run")) as (
184
182
  drv,
@@ -188,7 +186,7 @@ async def test_later_rewind_supersedes_the_staged_one():
188
186
  first = drv.post("run/edit", edit_params("m1", msg("m2"), anchor="p0"))
189
187
  await drv.wait(lambda s: "nextDispatch" in (s["runs"] or [{}])[0])
190
188
  second = drv.post("run/reload", reload_params("a1", anchor="m1"))
191
- assert_rejected(await drv.res(first), "superseded")
189
+ assert (await drv.res(first))["type"] == "accepted"
192
190
  await drv.wait(
193
191
  lambda s: s["runs"][0]["nextDispatch"]["trigger"] == "message-reload"
194
192
  )
@@ -201,7 +199,7 @@ async def test_later_rewind_supersedes_the_staged_one():
201
199
  rerun.finish(RunManager.Complete())
202
200
 
203
201
 
204
- async def test_stop_supersedes_a_staged_rewind():
202
+ async def test_stop_clears_a_staged_rewind():
205
203
  script = Script()
206
204
  async with run_host(script, capabilities=("rewind", "rewind-during-run")) as (
207
205
  drv,
@@ -210,12 +208,12 @@ async def test_stop_supersedes_a_staged_rewind():
210
208
  call = await start_run(drv, script)
211
209
  seq = drv.post("run/edit", edit_params("m1", msg("m2"), anchor="p0"))
212
210
  await drv.wait(lambda s: "nextDispatch" in (s["runs"] or [{}])[0])
213
- pending = await drv.stop(terminal=False)
214
- assert_rejected(await drv.res(seq), "superseded")
211
+ assert (await drv.res(seq))["type"] == "accepted"
212
+ settled = await drv.stop()
213
+ assert settled["type"] == "accepted"
215
214
  await drv.wait(lambda s: (s["runs"] or [{}])[0].get("stopping") == {"reason": "stop"})
216
215
  assert "nextDispatch" not in drv.run
217
216
  call.finish(RunManager.Stop(dispatch_queue=False))
218
- assert (await drv.res(pending["seq"]))["type"] == "accepted"
219
217
  await drv.wait_status("stopped")
220
218
  script.no_call()
221
219