harness-sdk-python 0.7.1__tar.gz → 0.7.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.
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/PKG-INFO +2 -1
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/pyproject.toml +2 -1
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/src/harness_sdk/run_manager.py +90 -74
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_ack_visibility.py +26 -23
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_batches.py +18 -3
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_dispatching.py +4 -4
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_facade.py +9 -2
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_settle.py +15 -13
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_stop_continue.py +2 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/.gitignore +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/README.md +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/src/harness_sdk/linear_thread.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/run_helpers.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_branch_anchor.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_edit_dispatched.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_edit_reload.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_enqueue.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_input_required.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_linear_thread.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_meta.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_outcomes.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_placement.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_prepare_hooks.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_rewind_during_run.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_run_leaf.py +0 -0
- {harness_sdk_python-0.7.1 → harness_sdk_python-0.7.2}/tests/test_steer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: harness-sdk-python
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -9,6 +9,7 @@ Requires-Dist: statewire<0.5,>=0.4.0
|
|
|
9
9
|
Provides-Extra: deepagents
|
|
10
10
|
Requires-Dist: deepagents>=0.6.12; extra == 'deepagents'
|
|
11
11
|
Requires-Dist: langchain-core>=0.3; extra == 'deepagents'
|
|
12
|
+
Requires-Dist: langchain-openai>=0.3; extra == 'deepagents'
|
|
12
13
|
Requires-Dist: langgraph>=1.0.0; extra == 'deepagents'
|
|
13
14
|
Requires-Dist: uvicorn[standard]>=0.30.0; extra == 'deepagents'
|
|
14
15
|
Provides-Extra: postgres
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "harness-sdk-python"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.2"
|
|
4
4
|
description = "RunManager: the harness-sdk runs subsystem for Python Statewire hosts"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -13,6 +13,7 @@ deepagents = [
|
|
|
13
13
|
"deepagents>=0.6.12",
|
|
14
14
|
"langgraph>=1.0.0",
|
|
15
15
|
"langchain-core>=0.3",
|
|
16
|
+
"langchain-openai>=0.3",
|
|
16
17
|
"uvicorn[standard]>=0.30.0",
|
|
17
18
|
]
|
|
18
19
|
|
|
@@ -4,16 +4,20 @@ Implements the full state x command matrix from runs.mdx on a single
|
|
|
4
4
|
``run(ctx)`` executor entrypoint, per the RunManager design (d4987).
|
|
5
5
|
|
|
6
6
|
Commands stage entries into an intake list and schedule a drain; the drain
|
|
7
|
-
reduces the whole intake
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
reduces the whole intake in order, then takes exactly one action based on
|
|
8
|
+
status and net effect (interrupt, dispatch, or continue). A dispatchable send
|
|
9
|
+
appends straight into ``dispatching`` — it never transits a queue lane — and
|
|
10
|
+
everything staged there by the end of the drain dispatches as one run. Every
|
|
11
|
+
send acks and settles accepted with its state-visible insert (lane or
|
|
12
|
+
``dispatching``); edit, reload, and continue initiators ack at their dispatch
|
|
13
|
+
and settle when their run ends — accepted once the run has acked its
|
|
14
|
+
messages, rejected otherwise; ``run/stop`` awaits a future the drain resolves
|
|
15
|
+
once the in-flight run has ended.
|
|
16
|
+
|
|
17
|
+
``ctx.ack_messages()`` is bookkeeping: it clears ``dispatching`` and confirms
|
|
18
|
+
``runLeafMessageId``. A run that ends with entries still in ``dispatching``
|
|
19
|
+
reverts them by provenance: lane items to the front of their lane, direct
|
|
20
|
+
dispatches to the front of the queue, rewind replacements dropped.
|
|
17
21
|
"""
|
|
18
22
|
|
|
19
23
|
import asyncio
|
|
@@ -130,10 +134,9 @@ class _Rewind:
|
|
|
130
134
|
@dataclass
|
|
131
135
|
class _Effects:
|
|
132
136
|
steer_added: bool = False
|
|
133
|
-
new_added: bool = False
|
|
134
137
|
continue_requested: bool = False
|
|
135
138
|
continue_meta: Any = None
|
|
136
|
-
staged_sends: list[tuple[
|
|
139
|
+
staged_sends: list[tuple["asyncio.Future[Any]", Callable[[], None]]] = field(
|
|
137
140
|
default_factory=list
|
|
138
141
|
)
|
|
139
142
|
continues: list["asyncio.Future[Any]"] = field(default_factory=list)
|
|
@@ -193,13 +196,8 @@ class RunManager:
|
|
|
193
196
|
self._stop_reason: str | None = None
|
|
194
197
|
self._staged_rewinds: list[_Rewind] = []
|
|
195
198
|
self._dispatching: list[tuple[str, dict[str, Any]]] = []
|
|
196
|
-
|
|
197
|
-
# the command's effect (message-ack append, or the revert re-insert).
|
|
198
|
-
self._deferred_acks: dict[str, Callable[[], None]] = {}
|
|
199
|
-
self._dispatch_ack: Callable[[], None] | None = None
|
|
200
|
-
self._run_acked = True
|
|
199
|
+
self._run_acked = False
|
|
201
200
|
self._leaf_confirmed: str | None = None
|
|
202
|
-
self._send_futures: list["asyncio.Future[Any]"] = []
|
|
203
201
|
self._run_futures: list["asyncio.Future[Any]"] = []
|
|
204
202
|
self._input_requests: list[dict[str, Any]] = []
|
|
205
203
|
self._input_answers: dict[str, Any] = {}
|
|
@@ -294,7 +292,6 @@ class RunManager:
|
|
|
294
292
|
entries, self._intake = self._intake, []
|
|
295
293
|
outcome, self._outcome = self._outcome, None
|
|
296
294
|
fx = _Effects()
|
|
297
|
-
pre_empty = not self._lane_items("steerQueue") and not self._lane_items("queue")
|
|
298
295
|
for entry in entries:
|
|
299
296
|
self._apply(entry, fx)
|
|
300
297
|
if self._task is None:
|
|
@@ -307,31 +304,28 @@ class RunManager:
|
|
|
307
304
|
if self._staged_rewinds:
|
|
308
305
|
rewind = self._staged_rewinds.pop(0)
|
|
309
306
|
self._clear_input()
|
|
307
|
+
if self._dispatching:
|
|
308
|
+
self._revert_dispatching()
|
|
310
309
|
self._dispatch(
|
|
311
310
|
rewind.type,
|
|
312
311
|
rewind.messages,
|
|
313
312
|
rollback_to=rewind.rollback_to,
|
|
314
313
|
root_meta=rewind.root_meta,
|
|
315
314
|
)
|
|
316
|
-
|
|
315
|
+
rewind.ack()
|
|
317
316
|
if not rewind.future.done():
|
|
318
317
|
self._run_futures.append(rewind.future)
|
|
319
318
|
elif outcome is not None:
|
|
320
319
|
self._settle_outcome(outcome)
|
|
321
320
|
else:
|
|
322
|
-
self._idle_action(fx
|
|
321
|
+
self._idle_action(fx)
|
|
323
322
|
if self._task is not None and (self._staged_stops or self._staged_rewinds):
|
|
324
323
|
assert self._ctx is not None
|
|
325
324
|
self._ctx.stop_requested.set()
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if
|
|
329
|
-
|
|
330
|
-
self._send_futures.append(future)
|
|
331
|
-
else:
|
|
332
|
-
ack()
|
|
333
|
-
if not future.done():
|
|
334
|
-
future.set_result(None)
|
|
325
|
+
for future, ack in fx.staged_sends:
|
|
326
|
+
ack()
|
|
327
|
+
if not future.done():
|
|
328
|
+
future.set_result(None)
|
|
335
329
|
for future in fx.continues:
|
|
336
330
|
if not future.done():
|
|
337
331
|
future.set_result(None)
|
|
@@ -366,19 +360,22 @@ class RunManager:
|
|
|
366
360
|
if result is not _PARKED and not entry.future.done():
|
|
367
361
|
entry.future.set_result(result)
|
|
368
362
|
|
|
369
|
-
def _idle_action(self, fx: _Effects
|
|
363
|
+
def _idle_action(self, fx: _Effects) -> None:
|
|
370
364
|
status = self._status()
|
|
371
365
|
if status == "ready":
|
|
372
|
-
self.
|
|
366
|
+
if self._dispatching:
|
|
367
|
+
self._dispatch_staged()
|
|
373
368
|
elif status in ("error", "stopped"):
|
|
374
369
|
if fx.continue_requested or fx.steer_added:
|
|
375
370
|
self._dispatch(self._continue_type(), [], root_meta=fx.continue_meta)
|
|
376
371
|
self._run_futures.extend(fx.continues)
|
|
377
372
|
fx.continues.clear()
|
|
378
|
-
elif
|
|
379
|
-
self.
|
|
373
|
+
elif self._dispatching:
|
|
374
|
+
self._dispatch_staged()
|
|
380
375
|
elif status == "input-required":
|
|
381
|
-
if
|
|
376
|
+
if self._dispatching:
|
|
377
|
+
self._dispatch_staged(input_outcomes=self._take_input_outcomes())
|
|
378
|
+
elif fx.steer_added and self._lane_items("steerQueue"):
|
|
382
379
|
self._pop_dispatchable(input_outcomes=self._take_input_outcomes())
|
|
383
380
|
elif self._input_requests and len(self._input_answers) == len(
|
|
384
381
|
self._input_requests
|
|
@@ -493,18 +490,14 @@ class RunManager:
|
|
|
493
490
|
"run must return a RunManager outcome, got "
|
|
494
491
|
f"{type(outcome).__name__}"
|
|
495
492
|
)
|
|
496
|
-
if isinstance(
|
|
497
|
-
outcome, (RunManager.Complete, RunManager.InputRequired)
|
|
498
|
-
) and (self._dispatching or not self._run_acked):
|
|
499
|
-
raise RuntimeError(
|
|
500
|
-
"run settled without acking its messages (call ctx.ack_messages())"
|
|
501
|
-
)
|
|
502
493
|
except asyncio.CancelledError:
|
|
503
494
|
await self._pull_leaf()
|
|
504
495
|
self._settle(ctx)
|
|
505
496
|
self._set_status("stopped")
|
|
506
497
|
self._entry()["runId"] = None
|
|
507
|
-
self._settle_initiators(
|
|
498
|
+
self._settle_initiators(
|
|
499
|
+
None if self._run_acked else _reject("stopped", "run cancelled")
|
|
500
|
+
)
|
|
508
501
|
self._revert_dispatching()
|
|
509
502
|
self._idle.set()
|
|
510
503
|
raise # no drain, no freeze
|
|
@@ -514,16 +507,20 @@ class RunManager:
|
|
|
514
507
|
message = str(exc) or type(exc).__name__
|
|
515
508
|
if isinstance(exc, StatewireReject):
|
|
516
509
|
self._freeze(exc.message, exc.payload)
|
|
517
|
-
self._settle_initiators(exc)
|
|
510
|
+
self._settle_initiators(None if self._run_acked else exc)
|
|
518
511
|
else:
|
|
519
512
|
self._freeze(message)
|
|
520
|
-
self._settle_initiators(
|
|
513
|
+
self._settle_initiators(
|
|
514
|
+
None if self._run_acked else _reject("run-error", message)
|
|
515
|
+
)
|
|
521
516
|
self._revert_dispatching()
|
|
522
517
|
self._drain()
|
|
523
518
|
return
|
|
524
519
|
await self._pull_leaf()
|
|
525
520
|
self._settle(ctx)
|
|
526
|
-
if
|
|
521
|
+
if self._run_acked:
|
|
522
|
+
error = None
|
|
523
|
+
elif isinstance(outcome, RunManager.Error):
|
|
527
524
|
error = _reject("run-error", "run ended in error")
|
|
528
525
|
elif isinstance(outcome, RunManager.Stop):
|
|
529
526
|
error = _reject("stopped", "run stopped before the messages-ack")
|
|
@@ -535,8 +532,8 @@ class RunManager:
|
|
|
535
532
|
self._drain()
|
|
536
533
|
|
|
537
534
|
async def _pull_leaf(self) -> None:
|
|
538
|
-
# An unacked
|
|
539
|
-
if self.
|
|
535
|
+
# An end with unacked messages reverts instead of recording a leaf.
|
|
536
|
+
if plain(self._entry()["dispatching"]):
|
|
540
537
|
return
|
|
541
538
|
self._entry()["runLeafMessageId"] = await self._thread.get_leaf_message_id()
|
|
542
539
|
|
|
@@ -545,7 +542,6 @@ class RunManager:
|
|
|
545
542
|
self._ctx = None
|
|
546
543
|
self._task = None
|
|
547
544
|
self._dispatch_record = None
|
|
548
|
-
self._dispatch_ack = None
|
|
549
545
|
|
|
550
546
|
def _settle_initiators(self, error: StatewireReject | None) -> None:
|
|
551
547
|
futures, self._run_futures = self._run_futures, []
|
|
@@ -557,29 +553,13 @@ class RunManager:
|
|
|
557
553
|
else:
|
|
558
554
|
future.set_exception(error)
|
|
559
555
|
|
|
560
|
-
def _settle_sends(self) -> None:
|
|
561
|
-
futures, self._send_futures = self._send_futures, []
|
|
562
|
-
for future in futures:
|
|
563
|
-
if not future.done():
|
|
564
|
-
future.set_result(None)
|
|
565
|
-
|
|
566
556
|
def _ack_messages(self) -> None:
|
|
567
|
-
if self._dispatch_ack is not None:
|
|
568
|
-
self._dispatch_ack()
|
|
569
|
-
self._dispatch_ack = None
|
|
570
|
-
for _, item in self._dispatching:
|
|
571
|
-
ack = self._deferred_acks.pop(item["id"], None)
|
|
572
|
-
if ack is not None:
|
|
573
|
-
ack()
|
|
574
557
|
self._dispatching = []
|
|
575
558
|
self._entry()["dispatching"] = []
|
|
576
559
|
self._run_acked = True
|
|
577
560
|
self._leaf_confirmed = plain(self._entry()["runLeafMessageId"])
|
|
578
|
-
self._settle_sends()
|
|
579
|
-
self._settle_initiators(None)
|
|
580
561
|
|
|
581
562
|
def _revert_dispatching(self) -> None:
|
|
582
|
-
self._settle_sends()
|
|
583
563
|
taken, self._dispatching = self._dispatching, []
|
|
584
564
|
entry = self._ensure_entry()
|
|
585
565
|
entry["dispatching"] = []
|
|
@@ -589,15 +569,49 @@ class RunManager:
|
|
|
589
569
|
front = [item for taken_lane, item in taken if taken_lane == lane]
|
|
590
570
|
if front:
|
|
591
571
|
entry[lane] = front + self._lane_items(lane)
|
|
592
|
-
for _, item in taken:
|
|
593
|
-
ack = self._deferred_acks.pop(item["id"], None)
|
|
594
|
-
if ack is not None:
|
|
595
|
-
ack()
|
|
596
572
|
ids = {item["id"] for _, item in taken}
|
|
597
573
|
self._dispatched_ids = tuple(id for id in self._dispatched_ids if id not in ids)
|
|
598
574
|
if plain(entry["runLeafMessageId"]) in ids:
|
|
599
575
|
entry["runLeafMessageId"] = self._leaf_confirmed
|
|
600
576
|
|
|
577
|
+
def _dispatchable(self, lane: str) -> bool:
|
|
578
|
+
if self._task is not None or self._staged_rewinds:
|
|
579
|
+
return False
|
|
580
|
+
if self._dispatching:
|
|
581
|
+
return True
|
|
582
|
+
status = self._status()
|
|
583
|
+
if status == "ready":
|
|
584
|
+
return True
|
|
585
|
+
if status == "input-required":
|
|
586
|
+
return lane == "steerQueue"
|
|
587
|
+
if status in ("error", "stopped"):
|
|
588
|
+
return (
|
|
589
|
+
lane == "queue"
|
|
590
|
+
and not self._lane_items("queue")
|
|
591
|
+
and not self._lane_items("steerQueue")
|
|
592
|
+
)
|
|
593
|
+
return False
|
|
594
|
+
|
|
595
|
+
def _stage_dispatch(self, e: _Send) -> None:
|
|
596
|
+
assert e.message is not None
|
|
597
|
+
steer = self._lane_items("steerQueue") if e.lane == "steerQueue" else []
|
|
598
|
+
if len(self._dispatching) + len(steer) >= self._max_queued:
|
|
599
|
+
raise _reject("queue-full", f"queue is full ({self._max_queued} messages)")
|
|
600
|
+
if steer:
|
|
601
|
+
self._entry()["steerQueue"] = []
|
|
602
|
+
self._dispatching.extend(("steerQueue", item) for item in steer)
|
|
603
|
+
self._dispatching.append(("queue", self._stamped(e.message, e.meta)))
|
|
604
|
+
self._ensure_entry()["dispatching"] = [item for _, item in self._dispatching]
|
|
605
|
+
|
|
606
|
+
def _dispatch_staged(
|
|
607
|
+
self, *, input_outcomes: tuple[tuple[dict[str, Any], Any, Any], ...] = ()
|
|
608
|
+
) -> None:
|
|
609
|
+
self._dispatch(
|
|
610
|
+
"message-send",
|
|
611
|
+
[item for _, item in self._dispatching],
|
|
612
|
+
input_outcomes=input_outcomes,
|
|
613
|
+
)
|
|
614
|
+
|
|
601
615
|
def _pop_dispatchable(
|
|
602
616
|
self, *, input_outcomes: tuple[tuple[dict[str, Any], Any, Any], ...] = ()
|
|
603
617
|
) -> bool:
|
|
@@ -751,7 +765,7 @@ class RunManager:
|
|
|
751
765
|
return
|
|
752
766
|
if self._lane_of(e.anchor) is not None:
|
|
753
767
|
return
|
|
754
|
-
if e.anchor in self._dispatched_ids:
|
|
768
|
+
if e.anchor in self._dispatched_ids or self._is_dispatching(e.anchor):
|
|
755
769
|
return
|
|
756
770
|
if e.anchor_meta is None:
|
|
757
771
|
raise _reject("unknown-id", f"anchor {e.anchor} names nothing")
|
|
@@ -829,11 +843,13 @@ class RunManager:
|
|
|
829
843
|
if e.anchor is _ABSENT:
|
|
830
844
|
raise _reject("invalid-message", "anchorMessageId is required")
|
|
831
845
|
self._check_anchor(e)
|
|
832
|
-
self.
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
846
|
+
if self._dispatchable(e.lane):
|
|
847
|
+
self._stage_dispatch(e)
|
|
848
|
+
else:
|
|
849
|
+
self._insert_new(e.lane, e.message, e.params, e.meta)
|
|
850
|
+
if e.lane == "steerQueue":
|
|
851
|
+
fx.steer_added = True
|
|
852
|
+
fx.staged_sends.append((e.future, e.ack))
|
|
837
853
|
return _PARKED
|
|
838
854
|
|
|
839
855
|
def _apply_move(self, e: _Send, fx: _Effects) -> Any:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
"""Contract:
|
|
2
|
-
|
|
3
|
-
``
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
"""Contract: acks ride visibility. A send's covering ack only rides an
|
|
2
|
+
envelope whose state carries the command's effect — the item in a published
|
|
3
|
+
queue lane or in ``dispatching``. An idle send's insert lands in
|
|
4
|
+
``dispatching`` directly, so its ack rides the dispatch envelope and the queue
|
|
5
|
+
lane never holds the item; a rewind's ack rides its dispatch the same way."""
|
|
6
6
|
|
|
7
7
|
import copy
|
|
8
8
|
|
|
@@ -22,26 +22,28 @@ def covered_state(drv, seq):
|
|
|
22
22
|
raise AssertionError(f"no envelope covers seq {seq}")
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def
|
|
25
|
+
def dispatching_ids(state):
|
|
26
26
|
runs = state.get("runs") or [{}]
|
|
27
|
-
return
|
|
28
|
-
*(m["id"] for m in state.get("messages", [])),
|
|
29
|
-
*(item["id"] for item in runs[0].get("queue", [])),
|
|
30
|
-
*(item["id"] for item in runs[0].get("steerQueue", [])),
|
|
31
|
-
}
|
|
27
|
+
return [item["id"] for item in runs[0].get("dispatching", [])]
|
|
32
28
|
|
|
33
29
|
|
|
34
|
-
|
|
30
|
+
def assert_never_queued(drv, message_id):
|
|
31
|
+
state: dict = {}
|
|
32
|
+
for env in copy.deepcopy(drv.envelopes):
|
|
33
|
+
apply_ops(state, env)
|
|
34
|
+
assert message_id not in queue_ids(state, "queue")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def test_idle_send_acks_with_its_dispatching_insert():
|
|
35
38
|
script = Script()
|
|
36
39
|
async with run_host(script) as (drv, host):
|
|
37
40
|
seq = drv.post("run/enqueue", add("m1"))
|
|
38
41
|
call = await script.next_call()
|
|
39
|
-
await drv.
|
|
40
|
-
assert
|
|
42
|
+
assert (await drv.res(seq))["type"] == "accepted"
|
|
43
|
+
assert dispatching_ids(covered_state(drv, seq)) == ["m1"]
|
|
44
|
+
assert_never_queued(drv, "m1")
|
|
41
45
|
host.live.state["messages"] = [msg("m1")]
|
|
42
46
|
call.ack()
|
|
43
|
-
assert (await drv.res(seq))["type"] == "accepted"
|
|
44
|
-
assert "m1" in lane_and_message_ids(covered_state(drv, seq))
|
|
45
47
|
call.finish(RunManager.Complete())
|
|
46
48
|
|
|
47
49
|
|
|
@@ -57,17 +59,19 @@ async def test_parked_send_acks_with_its_queue_insert():
|
|
|
57
59
|
call.finish(RunManager.Complete())
|
|
58
60
|
|
|
59
61
|
|
|
60
|
-
async def
|
|
62
|
+
async def test_unacked_run_end_reverts_the_direct_dispatch_to_the_queue():
|
|
61
63
|
script = Script()
|
|
62
64
|
async with run_host(script) as (drv, host):
|
|
63
65
|
seq = drv.post("run/enqueue", add("m1"))
|
|
64
66
|
call = await script.next_call()
|
|
65
|
-
call.fail(RuntimeError("boom"))
|
|
66
67
|
assert (await drv.res(seq))["type"] == "accepted"
|
|
67
|
-
assert
|
|
68
|
+
assert dispatching_ids(covered_state(drv, seq)) == ["m1"]
|
|
69
|
+
call.fail(RuntimeError("boom"))
|
|
70
|
+
await drv.wait_status("error")
|
|
71
|
+
assert queue_ids(drv.replica, "queue") == ["m1"]
|
|
68
72
|
|
|
69
73
|
|
|
70
|
-
async def
|
|
74
|
+
async def test_rewind_edit_acks_with_its_dispatch():
|
|
71
75
|
script = Script()
|
|
72
76
|
script.thread["u1"] = {"parentId": None, "role": "user", "isLeaf": True}
|
|
73
77
|
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
@@ -75,9 +79,8 @@ async def test_rewind_edit_acks_with_the_message_ack_append():
|
|
|
75
79
|
rerun = await script.next_call()
|
|
76
80
|
assert rerun.ctx.trigger == "message-edit"
|
|
77
81
|
await drv.wait_status("running")
|
|
78
|
-
assert
|
|
82
|
+
assert dispatching_ids(covered_state(drv, seq)) == ["u2"]
|
|
79
83
|
host.live.state["messages"] = [msg("u2")]
|
|
80
84
|
rerun.ack()
|
|
81
|
-
assert (await drv.res(seq))["type"] == "accepted"
|
|
82
|
-
assert "u2" in lane_and_message_ids(covered_state(drv, seq))
|
|
83
85
|
rerun.finish(RunManager.Complete())
|
|
86
|
+
assert (await drv.res(seq))["type"] == "accepted"
|
|
@@ -31,6 +31,21 @@ async def test_multi_steer_batch_places_all_and_dispatches_once():
|
|
|
31
31
|
assert (await drv.res(first + offset))["type"] == "accepted"
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
async def test_same_tick_sends_dispatch_as_one_run():
|
|
35
|
+
script = Script()
|
|
36
|
+
async with run_host(script) as (drv, host):
|
|
37
|
+
first = drv.batch(
|
|
38
|
+
[("run/enqueue", add("m1")), ("run/enqueue", add("m2", anchor="m1"))]
|
|
39
|
+
)
|
|
40
|
+
call = await script.next_call()
|
|
41
|
+
assert [m["id"] for m in call.ctx.messages] == ["m1", "m2"]
|
|
42
|
+
script.no_call()
|
|
43
|
+
assert (await drv.res(first))["type"] == "accepted"
|
|
44
|
+
assert (await drv.res(first + 1))["type"] == "accepted"
|
|
45
|
+
call.ack()
|
|
46
|
+
call.finish(RunManager.Complete())
|
|
47
|
+
|
|
48
|
+
|
|
34
49
|
async def test_steer_and_stop_in_one_batch_nets_to_stop():
|
|
35
50
|
script = Script()
|
|
36
51
|
async with run_host(script) as (drv, host):
|
|
@@ -92,7 +107,7 @@ async def test_handler_tail_staging_decides_in_its_own_envelope():
|
|
|
92
107
|
)
|
|
93
108
|
)
|
|
94
109
|
# The decide ran outside the batch segment: its ops flush in an
|
|
95
|
-
# envelope of their own
|
|
96
|
-
assert "ack"
|
|
97
|
-
call.ack()
|
|
110
|
+
# envelope of their own, carrying the send's covering ack.
|
|
111
|
+
assert dispatch_env.get("ack", 0) >= seq
|
|
98
112
|
assert (await drv.res(seq))["type"] == "accepted"
|
|
113
|
+
call.ack()
|
|
@@ -64,10 +64,10 @@ async def test_ack_clears_dispatching_with_the_message_append():
|
|
|
64
64
|
seq = drv.post("run/enqueue", add("m1"))
|
|
65
65
|
call = await script.next_call()
|
|
66
66
|
await drv.wait_status("running")
|
|
67
|
+
assert (await drv.res(seq))["type"] == "accepted"
|
|
67
68
|
host.live.state["messages"] = [msg("m1")]
|
|
68
69
|
call.ack()
|
|
69
|
-
|
|
70
|
-
assert dispatching_ids(drv.replica) == []
|
|
70
|
+
await drv.wait(lambda s: dispatching_ids(s) == [])
|
|
71
71
|
assert residence(drv, "m1") == ["dispatching", "messages"]
|
|
72
72
|
call.finish(RunManager.Complete())
|
|
73
73
|
|
|
@@ -117,7 +117,7 @@ async def test_edit_replacement_rides_dispatching_until_ack():
|
|
|
117
117
|
assert dispatching_ids(drv.replica) == ["u2"]
|
|
118
118
|
host.live.state["messages"] = [msg("u2")]
|
|
119
119
|
rerun.ack()
|
|
120
|
-
|
|
121
|
-
assert dispatching_ids(drv.replica) == []
|
|
120
|
+
await drv.wait(lambda s: dispatching_ids(s) == [])
|
|
122
121
|
assert residence(drv, "u2") == ["dispatching", "messages"]
|
|
123
122
|
rerun.finish(RunManager.Complete())
|
|
123
|
+
assert (await drv.res(seq))["type"] == "accepted"
|
|
@@ -8,7 +8,7 @@ import asyncio
|
|
|
8
8
|
|
|
9
9
|
from run_helpers import Script
|
|
10
10
|
from statewire_helpers import statewire_client
|
|
11
|
-
from test_assistant_transport import _PostStream, data_stream_frames
|
|
11
|
+
from test_assistant_transport import _PostStream, data_stream_frames, state_ops
|
|
12
12
|
|
|
13
13
|
from statewire import AssistantTransport, StatewireReject, command
|
|
14
14
|
|
|
@@ -63,13 +63,20 @@ async def test_chat_holds_until_the_entity_run_settles_then_eofs():
|
|
|
63
63
|
assert stream.status == 200
|
|
64
64
|
call = await script.next_call()
|
|
65
65
|
assert call.ctx.trigger == "message-send"
|
|
66
|
+
instance = (await app.state.pinned_host.directory.get("t1")).instance
|
|
67
|
+
instance.state["messages"] = [dict(call.ctx.messages[0])]
|
|
66
68
|
call.ack()
|
|
67
69
|
await asyncio.sleep(0.05)
|
|
68
70
|
assert stream._task is not None and not stream._task.done()
|
|
69
71
|
call.finish(RunManager.Complete())
|
|
70
72
|
tail = await stream.until_closed()
|
|
71
73
|
frames = data_stream_frames(tail.decode())
|
|
72
|
-
assert all(prefix == "aui-state" for prefix, _ in frames)
|
|
74
|
+
assert frames and all(prefix == "aui-state" for prefix, _ in frames)
|
|
75
|
+
ops = state_ops(frames)
|
|
76
|
+
assert ops[0]["type"] == "set" and ops[0]["path"] == []
|
|
77
|
+
assert [m["id"] for m in ops[0]["value"]["messages"]] == [
|
|
78
|
+
call.ctx.messages[0]["id"]
|
|
79
|
+
]
|
|
73
80
|
|
|
74
81
|
|
|
75
82
|
async def test_chat_run_reject_is_one_error_frame_then_eof():
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
"""Contract: command settlement.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
"""Contract: command settlement. Every send settles accepted with its
|
|
2
|
+
state-visible insert — a lane entry or a direct ``dispatching`` entry. Edit,
|
|
3
|
+
reload, and continue settle when their run ends: accepted once the run has
|
|
4
|
+
acked its messages, rejected (``stopped``, ``run-error``, or the failure)
|
|
5
|
+
otherwise. A run that ends with unacked entries returns lane-origin items to
|
|
6
|
+
the front of their lane, direct dispatches to the front of the queue, and
|
|
7
|
+
lands any failure on ``state.error``."""
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
10
|
|
|
@@ -132,15 +132,17 @@ async def test_ack_removes_the_entries_for_good():
|
|
|
132
132
|
assert queue_ids(drv.replica, "queue") == []
|
|
133
133
|
|
|
134
134
|
|
|
135
|
-
async def
|
|
135
|
+
async def test_complete_without_ack_reverts_and_redispatches():
|
|
136
136
|
script = Script()
|
|
137
137
|
async with run_host(script) as (drv, host):
|
|
138
138
|
seq = drv.post("run/enqueue", add("m1"))
|
|
139
139
|
(await script.next_call()).finish(RunManager.Complete())
|
|
140
|
-
await
|
|
141
|
-
assert "
|
|
142
|
-
assert queue_ids(drv.replica, "queue") == ["m1"]
|
|
140
|
+
redo = await script.next_call()
|
|
141
|
+
assert [m["id"] for m in redo.ctx.messages] == ["m1"]
|
|
143
142
|
assert (await drv.res(seq))["type"] == "accepted"
|
|
143
|
+
redo.ack()
|
|
144
|
+
redo.finish(RunManager.Complete())
|
|
145
|
+
await drv.wait_status("ready")
|
|
144
146
|
|
|
145
147
|
|
|
146
148
|
async def test_dequeue_removes_a_parked_entry():
|
|
@@ -172,7 +174,7 @@ async def test_stop_lands_while_a_steer_is_parked():
|
|
|
172
174
|
assert queue_ids(drv.replica, "queue") == ["m1"]
|
|
173
175
|
|
|
174
176
|
|
|
175
|
-
async def
|
|
177
|
+
async def test_rewind_edit_settles_at_the_reruns_end():
|
|
176
178
|
script = Script()
|
|
177
179
|
script.thread["u1"] = {"parentId": None, "role": "user", "isLeaf": True}
|
|
178
180
|
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
@@ -180,8 +182,8 @@ async def test_rewind_edit_settles_at_the_reruns_ack():
|
|
|
180
182
|
rerun = await script.next_call()
|
|
181
183
|
assert rerun.ctx.trigger == "message-edit"
|
|
182
184
|
rerun.ack()
|
|
183
|
-
assert (await drv.res(seq))["type"] == "accepted"
|
|
184
185
|
rerun.finish(RunManager.Complete())
|
|
186
|
+
assert (await drv.res(seq))["type"] == "accepted"
|
|
185
187
|
|
|
186
188
|
|
|
187
189
|
async def test_rewind_edit_failure_settles_with_the_payload():
|
|
@@ -209,6 +209,7 @@ async def test_bare_continue_with_incomplete_continuation(end, continue_type):
|
|
|
209
209
|
assert continued.ctx.messages == ()
|
|
210
210
|
assert not continued.ctx.has_rollback
|
|
211
211
|
continued.ack()
|
|
212
|
+
continued.finish(RunManager.Complete())
|
|
212
213
|
assert (await drv.res(res["seq"]))["type"] == "accepted"
|
|
213
214
|
|
|
214
215
|
|
|
@@ -235,6 +236,7 @@ async def test_continue_with_steer_lane_needs_no_capability():
|
|
|
235
236
|
assert continued.ctx.trigger == "error-continue"
|
|
236
237
|
assert [m["id"] for m in continued.ctx.steering.take()] == ["s1"]
|
|
237
238
|
continued.ack()
|
|
239
|
+
continued.finish(RunManager.Complete())
|
|
238
240
|
assert (await drv.res(res["seq"]))["type"] == "accepted"
|
|
239
241
|
|
|
240
242
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|