harness-sdk-python 0.13.0__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.0 → harness_sdk_python-0.13.2}/PKG-INFO +1 -1
  2. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/pyproject.toml +1 -1
  3. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/src/harness_sdk/run_manager.py +24 -35
  4. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_ack_visibility.py +1 -1
  5. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_batches.py +8 -14
  6. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_branch_anchor.py +1 -1
  7. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_dispatching.py +2 -2
  8. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_edit_dispatched.py +4 -4
  9. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_edit_reload.py +7 -7
  10. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_enqueue.py +3 -3
  11. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_input_required.py +5 -5
  12. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_meta.py +2 -2
  13. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_outcomes.py +1 -1
  14. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_prepare_hooks.py +1 -1
  15. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_restore.py +2 -2
  16. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_rewind_during_run.py +6 -6
  17. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_settle.py +1 -1
  18. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_steer.py +1 -1
  19. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_stop_continue.py +1 -1
  20. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_stopping_window.py +32 -34
  21. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/.gitignore +0 -0
  22. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/README.md +0 -0
  23. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/examples/__init__.py +0 -0
  24. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/examples/runs_app.py +0 -0
  25. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/src/harness_sdk/__init__.py +0 -0
  26. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/src/harness_sdk/fenced_postgres.py +0 -0
  27. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/src/harness_sdk/linear_thread.py +0 -0
  28. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/run_helpers.py +0 -0
  29. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_fenced_postgres.py +0 -0
  30. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_linear_thread.py +0 -0
  31. {harness_sdk_python-0.13.0 → harness_sdk_python-0.13.2}/tests/test_placement.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: harness-sdk-python
3
- Version: 0.13.0
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.0"
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
@@ -53,9 +53,9 @@ _CAPABILITIES = frozenset(
53
53
  )
54
54
 
55
55
  _TRIGGERS = (
56
- "send-message",
57
- "edit-message",
58
- "reload-message",
56
+ "message-send",
57
+ "message-edit",
58
+ "message-reload",
59
59
  "input-resume",
60
60
  "error-continue",
61
61
  "stop-continue",
@@ -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:
@@ -641,7 +638,7 @@ class RunManager:
641
638
 
642
639
  def _send_trigger(self) -> str:
643
640
  status = self._status()
644
- return self._continue_type() if status in ("error", "stopped") else "send-message"
641
+ return self._continue_type() if status in ("error", "stopped") else "message-send"
645
642
 
646
643
  def _stage_dispatch(self, e: _Send) -> None:
647
644
  assert e.message is not None
@@ -673,27 +670,17 @@ class RunManager:
673
670
  if steer:
674
671
  self._entry()["steerQueue"] = []
675
672
  self._dispatching = [("steerQueue", item) for item in steer]
676
- self._dispatch("send-message", steer, input_outcomes=input_outcomes)
673
+ self._dispatch("message-send", steer, input_outcomes=input_outcomes)
677
674
  return True
678
675
  queue = self._lane_items("queue")
679
676
  if queue:
680
677
  self._entry()["queue"].pop(0)
681
678
  self._dispatching = [("queue", queue[0])]
682
- self._dispatch("send-message", [queue[0]], input_outcomes=input_outcomes)
679
+ self._dispatch("message-send", [queue[0]], input_outcomes=input_outcomes)
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")
@@ -989,7 +978,7 @@ class RunManager:
989
978
  self._check_anchor(e)
990
979
  self._stage_rewind(
991
980
  _Rewind(
992
- "edit-message",
981
+ "message-edit",
993
982
  [self._stamped(e.message, e.meta)],
994
983
  e.source_meta["parentId"],
995
984
  e.ack,
@@ -1044,7 +1033,7 @@ class RunManager:
1044
1033
  )
1045
1034
  self._stage_rewind(
1046
1035
  _Rewind(
1047
- "edit-message",
1036
+ "message-edit",
1048
1037
  [self._stamped(e.message, e.meta)],
1049
1038
  e.source_meta["parentId"],
1050
1039
  e.ack,
@@ -1057,7 +1046,7 @@ class RunManager:
1057
1046
  def _apply_reload(self, e: _Reload) -> Any:
1058
1047
  self._stage_rewind(
1059
1048
  _Rewind(
1060
- "reload-message",
1049
+ "message-reload",
1061
1050
  [],
1062
1051
  e.source_meta["parentId"],
1063
1052
  e.ack,
@@ -91,7 +91,7 @@ async def test_rewind_edit_acks_with_its_dispatch():
91
91
  async with run_host(script, capabilities=("rewind",)) as (drv, host):
92
92
  seq = drv.post("run/edit", edit_params("u1", msg("u2"), anchor=None))
93
93
  rerun = await script.next_call()
94
- assert rerun.ctx.trigger == "edit-message"
94
+ assert rerun.ctx.trigger == "message-edit"
95
95
  await drv.wait_status("running")
96
96
  assert dispatching_ids(covered_state(drv, seq)) == ["u2"]
97
97
  host.live.state["messages"] = [msg("u2")]
@@ -21,7 +21,7 @@ async def test_multi_steer_batch_places_all_and_dispatches_once():
21
21
  ]
22
22
  )
23
23
  call = await script.next_call()
24
- assert call.ctx.trigger == "send-message"
24
+ assert call.ctx.trigger == "message-send"
25
25
  assert [m["id"] for m in call.ctx.messages] == ["s1", "s2", "s3"]
26
26
  script.no_call()
27
27
  assert queue_ids(drv.replica, "steerQueue") == []
@@ -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
 
@@ -146,7 +140,7 @@ async def test_rewind_outranks_send_and_continue_in_one_batch():
146
140
  ]
147
141
  )
148
142
  rerun = await script.next_call()
149
- assert rerun.ctx.trigger == "edit-message"
143
+ assert rerun.ctx.trigger == "message-edit"
150
144
  assert [m["id"] for m in rerun.ctx.messages] == ["m2"]
151
145
  script.no_call()
152
146
  assert (await drv.res(first))["type"] == "accepted"
@@ -173,7 +167,7 @@ async def test_send_outranks_the_completing_input_answer():
173
167
  ]
174
168
  )
175
169
  run = await script.next_call()
176
- assert run.ctx.trigger == "send-message"
170
+ assert run.ctx.trigger == "message-send"
177
171
  assert [m["id"] for m in run.ctx.messages] == ["s1"]
178
172
  assert run.ctx.input_outcomes == ((request, {"output": "ok"}, None),)
179
173
  script.no_call()
@@ -196,7 +196,7 @@ async def test_post_reload_sends_anchor_on_the_reload_targets_parent():
196
196
  async with run_host(script, capabilities=("rewind",)) as (drv, host):
197
197
  drv.post("run/reload", reload_params("a1", anchor="u1"))
198
198
  call = await script.next_call()
199
- assert call.ctx.trigger == "reload-message"
199
+ assert call.ctx.trigger == "message-reload"
200
200
  call.ack()
201
201
  script.thread["a1"] = {
202
202
  "parentId": "u1", "role": "assistant", "isLeaf": True, "onActiveBranch": False
@@ -58,7 +58,7 @@ async def test_idle_send_dispatches_straight_into_dispatching():
58
58
  drv.post("run/enqueue", add("m1"))
59
59
  await script.next_call()
60
60
  await drv.wait_status("running")
61
- assert drv.run["dispatching"]["trigger"] == "send-message"
61
+ assert drv.run["dispatching"]["trigger"] == "message-send"
62
62
  assert dispatching_ids(drv.replica) == ["m1"]
63
63
  assert queue_ids(drv.replica, "queue") == []
64
64
  assert residence(drv, "m1") == ["dispatching"]
@@ -119,7 +119,7 @@ async def test_edit_replacement_rides_dispatching_until_ack():
119
119
  async with run_host(script, capabilities=("rewind",)) as (drv, host):
120
120
  seq = drv.post("run/edit", edit_params("u1", msg("u2"), anchor=None))
121
121
  rerun = await script.next_call()
122
- assert rerun.ctx.trigger == "edit-message"
122
+ assert rerun.ctx.trigger == "message-edit"
123
123
  await drv.wait_status("running")
124
124
  assert dispatching_ids(drv.replica) == ["u2"]
125
125
  host.live.state["messages"] = [msg("u2")]
@@ -19,7 +19,7 @@ async def test_edit_of_dispatched_item_stops_rewinds_reruns(command):
19
19
  await drv.wait_status("running")
20
20
  call.finish(RunManager.Stop(dispatch_queue=False))
21
21
  rerun = await script.next_call()
22
- assert rerun.ctx.trigger == "edit-message"
22
+ assert rerun.ctx.trigger == "message-edit"
23
23
  assert [m["parts"][0]["text"] for m in rerun.ctx.messages] == ["edited"]
24
24
  assert rerun.ctx.has_rollback
25
25
  assert rerun.ctx.rollback_to == "p0"
@@ -54,7 +54,7 @@ async def test_edit_of_dispatched_non_leaf_stops_and_reruns(command):
54
54
  seq = drv.post(command, target(message=msg("m1", "edited")))
55
55
  call.finish(RunManager.Stop(dispatch_queue=False))
56
56
  rerun = await script.next_call()
57
- assert rerun.ctx.trigger == "edit-message"
57
+ assert rerun.ctx.trigger == "message-edit"
58
58
  assert [m["parts"][0]["text"] for m in rerun.ctx.messages] == ["edited"]
59
59
  rerun.ack()
60
60
  rerun.finish(RunManager.Stop(dispatch_queue=False))
@@ -72,7 +72,7 @@ async def test_edit_of_dispatched_item_with_queued_items_stops_and_reruns():
72
72
  seq = drv.post("run/enqueue", target(message=msg("m1", "edited")))
73
73
  call.finish(RunManager.Stop(dispatch_queue=False))
74
74
  rerun = await script.next_call()
75
- assert rerun.ctx.trigger == "edit-message"
75
+ assert rerun.ctx.trigger == "message-edit"
76
76
  rerun.ack()
77
77
  rerun.finish(RunManager.Stop(dispatch_queue=False))
78
78
  assert (await drv.res(seq))["type"] == "accepted"
@@ -90,5 +90,5 @@ async def test_edit_of_dispatched_item_in_error_reruns_without_stop():
90
90
  await drv.wait_status("error")
91
91
  drv.post("run/enqueue", target(message=msg("m1", "edited")))
92
92
  rerun = await script.next_call()
93
- assert rerun.ctx.trigger == "edit-message"
93
+ assert rerun.ctx.trigger == "message-edit"
94
94
  assert rerun.ctx.rollback_to is None
@@ -27,7 +27,7 @@ async def test_edit_in_ready_dispatches_message_edit():
27
27
  async with run_host(script, capabilities=("rewind",)) as (drv, host):
28
28
  drv.post("run/edit", edit_params("u1", msg("u2", "v2"), anchor=None))
29
29
  call = await script.next_call()
30
- assert call.ctx.trigger == "edit-message"
30
+ assert call.ctx.trigger == "message-edit"
31
31
  assert [m["id"] for m in call.ctx.messages] == ["u2"]
32
32
  assert call.ctx.has_rollback
33
33
  assert call.ctx.rollback_to is None
@@ -71,7 +71,7 @@ async def test_deep_edit_with_queue_non_empty_reruns_and_keeps_queue():
71
71
  await drv.wait_status("error")
72
72
  drv.post("run/edit", edit_params("u1", msg("u2"), anchor=None))
73
73
  rerun = await script.next_call()
74
- assert rerun.ctx.trigger == "edit-message"
74
+ assert rerun.ctx.trigger == "message-edit"
75
75
  assert rerun.ctx.rollback_to is None
76
76
  assert [item["id"] for item in drv.run["queue"]] == ["m2"]
77
77
 
@@ -88,7 +88,7 @@ async def test_leaf_edit_in_error_accepted_with_queue_non_empty():
88
88
  await drv.wait_status("error")
89
89
  drv.post("run/edit", edit_params("m1", msg("u2"), anchor=None))
90
90
  call = await script.next_call()
91
- assert call.ctx.trigger == "edit-message"
91
+ assert call.ctx.trigger == "message-edit"
92
92
  assert call.ctx.rollback_to is None
93
93
  assert [item["id"] for item in drv.run["queue"]] == ["m2"]
94
94
 
@@ -140,7 +140,7 @@ async def test_reload_dispatches_with_empty_messages():
140
140
  async with run_host(script, capabilities=("rewind",)) as (drv, host):
141
141
  drv.post("run/reload", reload_params("a1", anchor="u1"))
142
142
  call = await script.next_call()
143
- assert call.ctx.trigger == "reload-message"
143
+ assert call.ctx.trigger == "message-reload"
144
144
  assert call.ctx.messages == ()
145
145
  assert call.ctx.rollback_to == "u1"
146
146
  call.ack()
@@ -205,7 +205,7 @@ async def test_deep_reload_with_queue_non_empty_reruns_and_keeps_queue():
205
205
  await drv.wait_status("error")
206
206
  drv.post("run/reload", reload_params("a0", anchor="u0"))
207
207
  call = await script.next_call()
208
- assert call.ctx.trigger == "reload-message"
208
+ assert call.ctx.trigger == "message-reload"
209
209
  assert call.ctx.rollback_to == "u0"
210
210
  assert [item["id"] for item in drv.run["queue"]] == ["m2"]
211
211
 
@@ -260,7 +260,7 @@ async def test_edit_anchor_assertion():
260
260
  script.no_call()
261
261
  drv.post("run/edit", edit_params("u1", msg("u2"), anchor=None))
262
262
  call = await script.next_call()
263
- assert call.ctx.trigger == "edit-message"
263
+ assert call.ctx.trigger == "message-edit"
264
264
  assert call.ctx.rollback_to is None
265
265
 
266
266
 
@@ -283,5 +283,5 @@ async def test_reload_anchor_assertion():
283
283
  script.no_call()
284
284
  drv.post("run/reload", reload_params("a1", anchor="u1"))
285
285
  call = await script.next_call()
286
- assert call.ctx.trigger == "reload-message"
286
+ assert call.ctx.trigger == "message-reload"
287
287
  assert call.ctx.rollback_to == "u1"
@@ -9,7 +9,7 @@ async def test_ready_add_dispatches_message_send():
9
9
  async with run_host(script) as (drv, host):
10
10
  seq = drv.post("run/enqueue", add("m1"))
11
11
  call = await script.next_call()
12
- assert call.ctx.trigger == "send-message"
12
+ assert call.ctx.trigger == "message-send"
13
13
  assert [m["id"] for m in call.ctx.messages] == ["m1"]
14
14
  assert not call.ctx.has_rollback
15
15
  await drv.wait_status("running")
@@ -31,7 +31,7 @@ async def test_running_add_becomes_queue_item():
31
31
  script.no_call()
32
32
  call.finish(RunManager.Complete())
33
33
  drain = await script.next_call()
34
- assert drain.ctx.trigger == "send-message"
34
+ assert drain.ctx.trigger == "message-send"
35
35
  assert [m["id"] for m in drain.ctx.messages] == ["m2"]
36
36
  drain.ack()
37
37
  drain.finish(RunManager.Complete())
@@ -69,7 +69,7 @@ async def test_steer_lane_drains_first_and_whole():
69
69
  await drv.command("run/steer", add("s2"), terminal=False)
70
70
  call.finish(RunManager.Complete())
71
71
  steered = await script.next_call()
72
- assert steered.ctx.trigger == "send-message"
72
+ assert steered.ctx.trigger == "message-send"
73
73
  assert [m["id"] for m in steered.ctx.messages] == ["s1", "s2"]
74
74
  steered.ack()
75
75
  steered.finish(RunManager.Complete())
@@ -322,7 +322,7 @@ async def test_edit_while_parked_clears_requests_and_reruns():
322
322
  await park(drv, script, [tool_call("r1")], anchor="a1")
323
323
  drv.post("run/edit", edit_params("u1", msg("u2"), anchor=None))
324
324
  rerun = await script.next_call()
325
- assert rerun.ctx.trigger == "edit-message"
325
+ assert rerun.ctx.trigger == "message-edit"
326
326
  assert rerun.ctx.rollback_to is None
327
327
  await drv.wait(lambda s: "inputRequests" not in run_of(s))
328
328
  rerun.ack()
@@ -337,7 +337,7 @@ async def test_reload_while_parked_clears_requests_and_reruns():
337
337
  await park(drv, script, [tool_call("r1")], anchor="a1")
338
338
  drv.post("run/reload", reload_params("a1", anchor="u1"))
339
339
  rerun = await script.next_call()
340
- assert rerun.ctx.trigger == "reload-message"
340
+ assert rerun.ctx.trigger == "message-reload"
341
341
  assert rerun.ctx.rollback_to == "u1"
342
342
  await drv.wait(lambda s: "inputRequests" not in run_of(s))
343
343
  rerun.ack()
@@ -351,7 +351,7 @@ async def test_steer_add_while_parked_abandons_input_and_dispatches():
351
351
  await park(drv, script, [tool_call("r1"), approval("r2")])
352
352
  drv.post("run/steer", add("s1"))
353
353
  run = await script.next_call()
354
- assert run.ctx.trigger == "send-message"
354
+ assert run.ctx.trigger == "message-send"
355
355
  assert [m["id"] for m in run.ctx.messages] == ["s1"]
356
356
  assert run.ctx.input_outcomes == (
357
357
  (tool_call("r1"), None, None),
@@ -394,7 +394,7 @@ async def test_promote_to_steer_while_parked_abandons_input_and_dispatches():
394
394
  script.no_call()
395
395
  await drv.command("run/steer", target(messageId="m2"))
396
396
  run = await script.next_call()
397
- assert run.ctx.trigger == "send-message"
397
+ assert run.ctx.trigger == "message-send"
398
398
  assert [m["id"] for m in run.ctx.messages] == ["m2"]
399
399
  assert run.ctx.input_outcomes == ((tool_call("r1"), None, None),)
400
400
  assert "inputRequests" not in drv.run
@@ -434,7 +434,7 @@ async def test_steer_add_with_partial_answers_hands_over_the_answered_subset():
434
434
  )
435
435
  drv.post("run/steer", add("s1"))
436
436
  run = await script.next_call()
437
- assert run.ctx.trigger == "send-message"
437
+ assert run.ctx.trigger == "message-send"
438
438
  assert [m["id"] for m in run.ctx.messages] == ["s1"]
439
439
  assert run.ctx.input_outcomes == (
440
440
  (tool_call("r1"), {"output": "ok"}, None),
@@ -137,7 +137,7 @@ async def test_edit_meta_rides_the_replacement_message():
137
137
  edit_params("u1", msg("u2", "v2"), anchor=None, meta={"edited": True}),
138
138
  )
139
139
  call = await script.next_call()
140
- assert call.ctx.trigger == "edit-message"
140
+ assert call.ctx.trigger == "message-edit"
141
141
  assert call.ctx.meta("u2") == {"edited": True}
142
142
  assert "meta" not in call.ctx.messages[0]
143
143
  assert call.ctx.meta() is None
@@ -151,7 +151,7 @@ async def test_reload_meta_rides_the_start_root():
151
151
  async with run_host(script, capabilities=("rewind",)) as (drv, _):
152
152
  drv.post("run/reload", reload_params("a1", anchor="u1", meta={"retry": 1}))
153
153
  call = await script.next_call()
154
- assert call.ctx.trigger == "reload-message"
154
+ assert call.ctx.trigger == "message-reload"
155
155
  assert call.ctx.meta() == {"retry": 1}
156
156
  call.ack()
157
157
  call.finish(RunManager.Complete())
@@ -13,7 +13,7 @@ async def test_register_overwrites_whatever_is_in_the_state():
13
13
  "error": {"message": "old"},
14
14
  "queue": [msg("q1")],
15
15
  "steerQueue": [msg("s1")],
16
- "dispatch": {"type": "send-message", "messages": [msg("m1")]},
16
+ "dispatch": {"type": "message-send", "messages": [msg("m1")]},
17
17
  }
18
18
  async with run_host(script, initial_runs=stale) as (drv, host):
19
19
  assert drv.replica == {"status": "ready", "runs": []}
@@ -94,7 +94,7 @@ async def test_prepare_message_applies_to_edit():
94
94
  ) as (drv, host):
95
95
  drv.post("run/edit", edit_params("u1", noisy("u2"), anchor=None))
96
96
  call = await script.next_call()
97
- assert call.ctx.trigger == "edit-message"
97
+ assert call.ctx.trigger == "message-edit"
98
98
  assert [m["parts"] for m in call.ctx.messages] == [
99
99
  [{"type": "text", "text": "u2"}]
100
100
  ]
@@ -89,7 +89,7 @@ async def test_steer_while_hydrated_abandons_input_and_dispatches():
89
89
  ):
90
90
  drv.post("run/steer", add("s1"))
91
91
  run = await script.next_call()
92
- assert run.ctx.trigger == "send-message"
92
+ assert run.ctx.trigger == "message-send"
93
93
  assert [m["id"] for m in run.ctx.messages] == ["s1"]
94
94
  assert run.ctx.input_outcomes == ((tool_call("r1"), None, None),)
95
95
  await drv.wait_status("running")
@@ -114,7 +114,7 @@ async def test_malformed_restore_states_raise():
114
114
  ({"runs": [parked_entry(error={"message": "x"})]}, "error"),
115
115
  ({"runs": [parked_entry(error=None)]}, "error"),
116
116
  ({"runs": [parked_entry(stopping={"reason": "stop"})]}, "stopping"),
117
- ({"runs": [parked_entry(nextDispatch={"trigger": "edit-message"})]}, "nextDispatch"),
117
+ ({"runs": [parked_entry(nextDispatch={"trigger": "message-edit"})]}, "nextDispatch"),
118
118
  ({"runs": [parked_entry(inputRequests=[])]}, "non-empty list"),
119
119
  ({"runs": [parked_entry(inputRequests=["nope"])]}, "objects"),
120
120
  (
@@ -33,7 +33,7 @@ async def test_running_leaf_edit_stops_settles_then_rewinds():
33
33
  await drv.wait_status("running")
34
34
  call.finish(RunManager.Stop(dispatch_queue=False))
35
35
  rerun = await script.next_call()
36
- assert rerun.ctx.trigger == "edit-message"
36
+ assert rerun.ctx.trigger == "message-edit"
37
37
  assert [m["id"] for m in rerun.ctx.messages] == ["m2"]
38
38
  assert rerun.ctx.rollback_to is None
39
39
  rerun.ack()
@@ -50,7 +50,7 @@ async def test_running_leaf_edit_accepted_with_non_empty_queues():
50
50
  seq = drv.post("run/edit", edit_params("m1", msg("m2"), anchor=None))
51
51
  call.finish(RunManager.Stop(dispatch_queue=False))
52
52
  rerun = await script.next_call()
53
- assert rerun.ctx.trigger == "edit-message"
53
+ assert rerun.ctx.trigger == "message-edit"
54
54
  rerun.ack()
55
55
  rerun.finish(RunManager.Stop(dispatch_queue=False))
56
56
  assert (await drv.res(seq))["type"] == "accepted"
@@ -67,7 +67,7 @@ async def test_running_leaf_reload_stops_settles_then_rewinds():
67
67
  await asyncio.wait_for(call.ctx.stop_requested.wait(), 5)
68
68
  call.finish(RunManager.Stop(dispatch_queue=False))
69
69
  rerun = await script.next_call()
70
- assert rerun.ctx.trigger == "reload-message"
70
+ assert rerun.ctx.trigger == "message-reload"
71
71
  assert rerun.ctx.rollback_to == "m1"
72
72
  rerun.ack()
73
73
  rerun.finish(RunManager.Stop(dispatch_queue=False))
@@ -97,7 +97,7 @@ async def test_running_non_leaf_edit_stops_settles_then_rewinds():
97
97
  await asyncio.wait_for(call.ctx.stop_requested.wait(), 5)
98
98
  call.finish(RunManager.Stop(dispatch_queue=False))
99
99
  rerun = await script.next_call()
100
- assert rerun.ctx.trigger == "edit-message"
100
+ assert rerun.ctx.trigger == "message-edit"
101
101
  assert [m["id"] for m in rerun.ctx.messages] == ["m2"]
102
102
  assert rerun.ctx.rollback_to is None
103
103
  rerun.ack()
@@ -113,7 +113,7 @@ async def test_running_non_leaf_reload_stops_settles_then_rewinds():
113
113
  await asyncio.wait_for(call.ctx.stop_requested.wait(), 5)
114
114
  call.finish(RunManager.Stop(dispatch_queue=False))
115
115
  rerun = await script.next_call()
116
- assert rerun.ctx.trigger == "reload-message"
116
+ assert rerun.ctx.trigger == "message-reload"
117
117
  assert rerun.ctx.rollback_to == "m1"
118
118
  rerun.ack()
119
119
  rerun.finish(RunManager.Stop(dispatch_queue=False))
@@ -142,5 +142,5 @@ async def test_leaf_reload_in_error_accepted_with_non_empty_queue():
142
142
  await drv.wait_status("error")
143
143
  drv.post("run/reload", reload_params("a1", anchor="m1"))
144
144
  rerun = await script.next_call()
145
- assert rerun.ctx.trigger == "reload-message"
145
+ assert rerun.ctx.trigger == "message-reload"
146
146
  assert queue_ids(drv.replica, "queue") == ["q1"]
@@ -176,7 +176,7 @@ async def test_rewind_edit_settles_at_its_dispatch():
176
176
  async with run_host(script, capabilities=("rewind",)) as (drv, host):
177
177
  seq = drv.post("run/edit", edit_params("u1", msg("u2"), anchor=None))
178
178
  rerun = await script.next_call()
179
- assert rerun.ctx.trigger == "edit-message"
179
+ assert rerun.ctx.trigger == "message-edit"
180
180
  assert (await drv.res(seq))["type"] == "accepted"
181
181
  rerun.ack()
182
182
  rerun.finish(RunManager.Complete())
@@ -9,7 +9,7 @@ async def test_ready_steer_add_dispatches_like_enqueue():
9
9
  async with run_host(script) as (drv, host):
10
10
  drv.post("run/steer", add("m1"))
11
11
  call = await script.next_call()
12
- assert call.ctx.trigger == "send-message"
12
+ assert call.ctx.trigger == "message-send"
13
13
  assert [m["id"] for m in call.ctx.messages] == ["m1"]
14
14
 
15
15
 
@@ -33,7 +33,7 @@ async def test_stop_outcome_can_keep_the_queue_draining():
33
33
  await drv.stop(terminal=False)
34
34
  call.finish(RunManager.Stop(dispatch_queue=True))
35
35
  drain = await script.next_call()
36
- assert drain.ctx.trigger == "send-message"
36
+ assert drain.ctx.trigger == "message-send"
37
37
  assert [m["id"] for m in drain.ctx.messages] == ["m2"]
38
38
  await drv.wait_status("running")
39
39
 
@@ -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
 
@@ -89,10 +87,10 @@ async def test_mid_run_edit_stages_the_window_and_promotes_in_one_envelope():
89
87
  await asyncio.wait_for(call.ctx.stop_requested.wait(), 5)
90
88
  await drv.wait(lambda s: "stopping" in (s["runs"] or [{}])[0])
91
89
  assert drv.replica["status"] == "running"
92
- assert drv.run["stopping"] == {"reason": "edit-message"}
90
+ assert drv.run["stopping"] == {"reason": "message-edit"}
93
91
  staged = drv.run["nextDispatch"]
94
92
  assert staged == {
95
- "trigger": "edit-message",
93
+ "trigger": "message-edit",
96
94
  "messages": [msg("m2")],
97
95
  "rollbackTo": "p0",
98
96
  }
@@ -101,12 +99,12 @@ async def test_mid_run_edit_stages_the_window_and_promotes_in_one_envelope():
101
99
  for env, state in states(drv)
102
100
  if any(r["seq"] == seq for r in env.get("res", []))
103
101
  )
104
- assert window_of(opened) == ({"reason": "edit-message"}, staged)
102
+ assert window_of(opened) == ({"reason": "message-edit"}, staged)
105
103
  call.finish(RunManager.Stop(dispatch_queue=False))
106
104
  rerun = await script.next_call()
107
- assert rerun.ctx.trigger == "edit-message"
105
+ assert rerun.ctx.trigger == "message-edit"
108
106
  assert rerun.ctx.rollback_to == "p0"
109
- await drv.wait(lambda s: (s["runs"][0].get("dispatching") or {}).get("trigger") == "edit-message")
107
+ await drv.wait(lambda s: (s["runs"][0].get("dispatching") or {}).get("trigger") == "message-edit")
110
108
  trail = [
111
109
  (window_of(state), (state["runs"] or [{}])[0].get("dispatching"))
112
110
  for _, state in states(drv)
@@ -114,11 +112,11 @@ async def test_mid_run_edit_stages_the_window_and_promotes_in_one_envelope():
114
112
  promoted = next(
115
113
  i
116
114
  for i, (_, record) in enumerate(trail)
117
- if record is not None and record["trigger"] == "edit-message"
115
+ if record is not None and record["trigger"] == "message-edit"
118
116
  )
119
117
  assert trail[promoted][0] == (None, None)
120
118
  assert trail[promoted][1] == staged
121
- assert trail[promoted - 1][0] == ({"reason": "edit-message"}, staged)
119
+ assert trail[promoted - 1][0] == ({"reason": "message-edit"}, staged)
122
120
  assert (await drv.res(seq))["type"] == "accepted"
123
121
  rerun.ack()
124
122
  rerun.finish(RunManager.Complete())
@@ -134,16 +132,16 @@ async def test_mid_run_reload_stages_reason_and_empty_messages():
134
132
  call = await start_run(drv, script)
135
133
  seq = drv.post("run/reload", reload_params("a1", anchor="m1", meta={"retry": 1}))
136
134
  await drv.wait(lambda s: "stopping" in (s["runs"] or [{}])[0])
137
- assert drv.run["stopping"] == {"reason": "reload-message"}
135
+ assert drv.run["stopping"] == {"reason": "message-reload"}
138
136
  assert drv.run["nextDispatch"] == {
139
- "trigger": "reload-message",
137
+ "trigger": "message-reload",
140
138
  "messages": [],
141
139
  "rollbackTo": "m1",
142
140
  "rootMeta": {"retry": 1},
143
141
  }
144
142
  call.finish(RunManager.Stop(dispatch_queue=False))
145
143
  rerun = await script.next_call()
146
- assert rerun.ctx.trigger == "reload-message"
144
+ assert rerun.ctx.trigger == "message-reload"
147
145
  assert rerun.ctx.meta() == {"retry": 1}
148
146
  await drv.wait(lambda s: "stopping" not in (s["runs"] or [{}])[0])
149
147
  assert drv.run["dispatching"]["rootMeta"] == {"retry": 1}
@@ -163,22 +161,22 @@ async def test_edit_of_dispatched_message_opens_the_window(command):
163
161
  seq = drv.post(command, target(message=msg("m1", "edited")))
164
162
  await drv.wait(lambda s: "stopping" in (s["runs"] or [{}])[0])
165
163
  assert drv.replica["status"] == "running"
166
- assert drv.run["stopping"] == {"reason": "edit-message"}
164
+ assert drv.run["stopping"] == {"reason": "message-edit"}
167
165
  assert drv.run["nextDispatch"] == {
168
- "trigger": "edit-message",
166
+ "trigger": "message-edit",
169
167
  "messages": [msg("m1", "edited")],
170
168
  "rollbackTo": "p0",
171
169
  }
172
170
  call.finish(RunManager.Stop(dispatch_queue=False))
173
171
  rerun = await script.next_call()
174
- assert rerun.ctx.trigger == "edit-message"
172
+ assert rerun.ctx.trigger == "message-edit"
175
173
  assert rerun.ctx.rollback_to == "p0"
176
174
  assert (await drv.res(seq))["type"] == "accepted"
177
175
  rerun.ack()
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,20 +186,20 @@ 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
- lambda s: s["runs"][0]["nextDispatch"]["trigger"] == "reload-message"
191
+ lambda s: s["runs"][0]["nextDispatch"]["trigger"] == "message-reload"
194
192
  )
195
- assert drv.run["stopping"] == {"reason": "reload-message"}
193
+ assert drv.run["stopping"] == {"reason": "message-reload"}
196
194
  call.finish(RunManager.Stop(dispatch_queue=False))
197
195
  rerun = await script.next_call()
198
- assert rerun.ctx.trigger == "reload-message"
196
+ assert rerun.ctx.trigger == "message-reload"
199
197
  assert (await drv.res(second))["type"] == "accepted"
200
198
  rerun.ack()
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
 
@@ -235,7 +233,7 @@ async def test_sends_during_the_window_park_in_their_lanes():
235
233
  assert queue_ids(drv.replica, "steerQueue") == ["s1"]
236
234
  call.finish(RunManager.Stop(dispatch_queue=False))
237
235
  rerun = await script.next_call()
238
- assert rerun.ctx.trigger == "edit-message"
236
+ assert rerun.ctx.trigger == "message-edit"
239
237
  assert (await drv.res(seq))["type"] == "accepted"
240
238
  assert queue_ids(drv.replica, "queue") == ["q1"]
241
239
  assert queue_ids(drv.replica, "steerQueue") == ["s1"]