harness-sdk-python 0.13.1__tar.gz → 0.13.3__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.13.1 → harness_sdk_python-0.13.3}/PKG-INFO +3 -3
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/examples/runs_app.py +7 -7
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/pyproject.toml +2 -2
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/src/harness_sdk/run_manager.py +55 -66
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/run_helpers.py +9 -9
- harness_sdk_python-0.13.1/tests/test_ack_visibility.py → harness_sdk_python-0.13.3/tests/test_applied_visibility.py +11 -11
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_batches.py +15 -21
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_branch_anchor.py +10 -10
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_dispatching.py +21 -21
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_edit_dispatched.py +8 -8
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_edit_reload.py +6 -6
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_enqueue.py +18 -18
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_input_required.py +20 -20
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_meta.py +16 -16
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_outcomes.py +9 -9
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_placement.py +1 -1
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_prepare_hooks.py +4 -4
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_restore.py +4 -4
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_rewind_during_run.py +6 -6
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_settle.py +14 -14
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_steer.py +11 -11
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_stop_continue.py +22 -22
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_stopping_window.py +22 -24
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/.gitignore +0 -0
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/README.md +0 -0
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/examples/__init__.py +0 -0
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/src/harness_sdk/linear_thread.py +0 -0
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.13.1 → harness_sdk_python-0.13.3}/tests/test_linear_thread.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: harness-sdk-python
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.3
|
|
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
|
|
7
7
|
Requires-Python: <4.0,>=3.11
|
|
8
|
-
Requires-Dist: statewire<0.
|
|
8
|
+
Requires-Dist: statewire<0.8,>=0.7.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'
|
|
@@ -107,7 +107,7 @@ class RunsHost(Statewire):
|
|
|
107
107
|
"content": user_text(message),
|
|
108
108
|
}
|
|
109
109
|
)
|
|
110
|
-
ctx.
|
|
110
|
+
ctx.applied()
|
|
111
111
|
outcome = await self.stream_turn(ctx)
|
|
112
112
|
if outcome is not None:
|
|
113
113
|
return outcome
|
|
@@ -158,11 +158,11 @@ class RunsHost(Statewire):
|
|
|
158
158
|
|
|
159
159
|
@command("run/enqueue")
|
|
160
160
|
async def run_enqueue(self, params, *, ctx):
|
|
161
|
-
return await self.runs.enqueue(params,
|
|
161
|
+
return await self.runs.enqueue(params, applied=ctx.applied)
|
|
162
162
|
|
|
163
163
|
@command("run/steer")
|
|
164
164
|
async def run_steer(self, params, *, ctx):
|
|
165
|
-
return await self.runs.steer(params,
|
|
165
|
+
return await self.runs.steer(params, applied=ctx.applied)
|
|
166
166
|
|
|
167
167
|
@command("run/dequeue")
|
|
168
168
|
async def run_dequeue(self, params):
|
|
@@ -170,19 +170,19 @@ class RunsHost(Statewire):
|
|
|
170
170
|
|
|
171
171
|
@command("run/edit")
|
|
172
172
|
async def run_edit(self, params, *, ctx):
|
|
173
|
-
return await self.runs.edit(params,
|
|
173
|
+
return await self.runs.edit(params, applied=ctx.applied)
|
|
174
174
|
|
|
175
175
|
@command("run/reload")
|
|
176
176
|
async def run_reload(self, params, *, ctx):
|
|
177
|
-
return await self.runs.reload(params,
|
|
177
|
+
return await self.runs.reload(params, applied=ctx.applied)
|
|
178
178
|
|
|
179
179
|
@command("run/stop")
|
|
180
180
|
async def run_stop(self, params=None, *, ctx):
|
|
181
|
-
return await self.runs.stop(params,
|
|
181
|
+
return await self.runs.stop(params, applied=ctx.applied)
|
|
182
182
|
|
|
183
183
|
@command("run/continue")
|
|
184
184
|
async def run_continue(self, params=None, *, ctx):
|
|
185
|
-
return await self.runs.continue_run(
|
|
185
|
+
return await self.runs.continue_run(applied=ctx.applied)
|
|
186
186
|
|
|
187
187
|
@command("run/input")
|
|
188
188
|
async def run_input(self, params):
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "harness-sdk-python"
|
|
3
|
-
version = "0.13.
|
|
3
|
+
version = "0.13.3"
|
|
4
4
|
description = "RunManager: the harness-sdk runs subsystem for Python Statewire hosts"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
requires-python = ">=3.11,<4.0"
|
|
8
|
-
dependencies = ["statewire>=0.
|
|
8
|
+
dependencies = ["statewire>=0.7.0,<0.8"]
|
|
9
9
|
|
|
10
10
|
[project.optional-dependencies]
|
|
11
11
|
postgres = ["langgraph-checkpoint-postgres>=2.0.0"]
|
|
@@ -10,23 +10,23 @@ 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
|
|
14
|
-
sends with their insert (lane
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
initiator applies 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
|
-
``entry["dispatching"]`` is the dispatch record (absent between an
|
|
18
|
+
``entry["dispatching"]`` is the dispatch record (absent between an applied and
|
|
19
19
|
the next dispatch or take): the payload the run callback receives.
|
|
20
|
-
``ctx.
|
|
21
|
-
|
|
20
|
+
``ctx.applied()`` clears it; ``steering.take()`` may refill it — at most one
|
|
21
|
+
unapplied batch ever exists. Settle consumes the delivered batch, applied or not;
|
|
22
22
|
steer entries still undelivered in the lane stay queued for the next run.
|
|
23
23
|
|
|
24
24
|
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
|
|
29
|
-
staged
|
|
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
|
|
@@ -87,7 +87,7 @@ class _Send:
|
|
|
87
87
|
message_id: str
|
|
88
88
|
source_meta: dict[str, Any] | None
|
|
89
89
|
meta: Any
|
|
90
|
-
|
|
90
|
+
applied: Callable[[], None]
|
|
91
91
|
run_id: str = ""
|
|
92
92
|
anchor: Any = _ABSENT
|
|
93
93
|
anchor_meta: dict[str, Any] | None = None
|
|
@@ -107,7 +107,7 @@ class _Edit:
|
|
|
107
107
|
source_meta: dict[str, Any]
|
|
108
108
|
message: dict[str, Any]
|
|
109
109
|
meta: Any
|
|
110
|
-
|
|
110
|
+
applied: Callable[[], None]
|
|
111
111
|
run_id: str
|
|
112
112
|
future: "asyncio.Future[Any]" = field(default_factory=_future)
|
|
113
113
|
|
|
@@ -116,7 +116,7 @@ class _Edit:
|
|
|
116
116
|
class _Reload:
|
|
117
117
|
source_meta: dict[str, Any]
|
|
118
118
|
meta: Any
|
|
119
|
-
|
|
119
|
+
applied: Callable[[], None]
|
|
120
120
|
run_id: str
|
|
121
121
|
future: "asyncio.Future[Any]" = field(default_factory=_future)
|
|
122
122
|
|
|
@@ -145,11 +145,11 @@ class _Rewind:
|
|
|
145
145
|
type: str
|
|
146
146
|
messages: list[dict[str, Any]]
|
|
147
147
|
rollback_to: Any
|
|
148
|
-
|
|
148
|
+
applied: Callable[[], None]
|
|
149
149
|
future: "asyncio.Future[Any]"
|
|
150
150
|
run_id: str
|
|
151
151
|
root_meta: Any = None
|
|
152
|
-
|
|
152
|
+
applied_sent: bool = False
|
|
153
153
|
|
|
154
154
|
|
|
155
155
|
@dataclass
|
|
@@ -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.
|
|
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
|
-
|
|
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:
|
|
@@ -376,21 +373,19 @@ class RunManager:
|
|
|
376
373
|
rollback_to=rewind.rollback_to,
|
|
377
374
|
root_meta=rewind.root_meta,
|
|
378
375
|
)
|
|
379
|
-
if not rewind.
|
|
380
|
-
rewind.
|
|
381
|
-
if not rewind.future.done():
|
|
382
|
-
rewind.future.set_result(None)
|
|
376
|
+
if not rewind.applied_sent:
|
|
377
|
+
rewind.applied()
|
|
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.
|
|
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()
|
|
392
|
-
for future,
|
|
393
|
-
|
|
387
|
+
for future, applied, result in fx.staged_sends:
|
|
388
|
+
applied()
|
|
394
389
|
if not future.done():
|
|
395
390
|
future.set_result(result)
|
|
396
391
|
for future in fx.continues:
|
|
@@ -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.
|
|
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:
|
|
@@ -604,11 +601,11 @@ class RunManager:
|
|
|
604
601
|
entry.pop("dispatching", None)
|
|
605
602
|
entry.pop("stopping", None)
|
|
606
603
|
|
|
607
|
-
def
|
|
604
|
+
def _applied(self) -> None:
|
|
608
605
|
entry = self._entry()
|
|
609
606
|
record = plain(entry.get("dispatching"))
|
|
610
607
|
if record is None:
|
|
611
|
-
raise RuntimeError("
|
|
608
|
+
raise RuntimeError("applied() with no unapplied batch")
|
|
612
609
|
self._dispatching = []
|
|
613
610
|
entry.pop("dispatching", None)
|
|
614
611
|
|
|
@@ -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()
|
|
@@ -704,8 +691,10 @@ class RunManager:
|
|
|
704
691
|
rollback_to=rewind.rollback_to,
|
|
705
692
|
root_meta=rewind.root_meta,
|
|
706
693
|
)
|
|
707
|
-
rewind.
|
|
708
|
-
rewind.
|
|
694
|
+
rewind.applied()
|
|
695
|
+
rewind.applied_sent = 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")
|
|
@@ -952,7 +941,7 @@ class RunManager:
|
|
|
952
941
|
if live_run_id is not None and e.anchor is not _ABSENT
|
|
953
942
|
else None
|
|
954
943
|
)
|
|
955
|
-
fx.staged_sends.append((e.future, e.
|
|
944
|
+
fx.staged_sends.append((e.future, e.applied, result))
|
|
956
945
|
return _PARKED
|
|
957
946
|
|
|
958
947
|
def _apply_move(self, e: _Send, fx: _Effects) -> Any:
|
|
@@ -992,7 +981,7 @@ class RunManager:
|
|
|
992
981
|
"message-edit",
|
|
993
982
|
[self._stamped(e.message, e.meta)],
|
|
994
983
|
e.source_meta["parentId"],
|
|
995
|
-
e.
|
|
984
|
+
e.applied,
|
|
996
985
|
e.future,
|
|
997
986
|
e.run_id,
|
|
998
987
|
)
|
|
@@ -1047,7 +1036,7 @@ class RunManager:
|
|
|
1047
1036
|
"message-edit",
|
|
1048
1037
|
[self._stamped(e.message, e.meta)],
|
|
1049
1038
|
e.source_meta["parentId"],
|
|
1050
|
-
e.
|
|
1039
|
+
e.applied,
|
|
1051
1040
|
e.future,
|
|
1052
1041
|
e.run_id,
|
|
1053
1042
|
)
|
|
@@ -1060,7 +1049,7 @@ class RunManager:
|
|
|
1060
1049
|
"message-reload",
|
|
1061
1050
|
[],
|
|
1062
1051
|
e.source_meta["parentId"],
|
|
1063
|
-
e.
|
|
1052
|
+
e.applied,
|
|
1064
1053
|
e.future,
|
|
1065
1054
|
e.run_id,
|
|
1066
1055
|
root_meta=e.meta,
|
|
@@ -1075,7 +1064,7 @@ class RunManager:
|
|
|
1075
1064
|
lane: str,
|
|
1076
1065
|
params: Any,
|
|
1077
1066
|
meta: Any,
|
|
1078
|
-
|
|
1067
|
+
applied: Callable[[], None],
|
|
1079
1068
|
) -> Any:
|
|
1080
1069
|
if not isinstance(params, dict):
|
|
1081
1070
|
raise _reject("invalid-message", "params must be an object")
|
|
@@ -1111,7 +1100,7 @@ class RunManager:
|
|
|
1111
1100
|
message_id,
|
|
1112
1101
|
None,
|
|
1113
1102
|
None,
|
|
1114
|
-
|
|
1103
|
+
applied,
|
|
1115
1104
|
run_id=run_id,
|
|
1116
1105
|
anchor=anchor,
|
|
1117
1106
|
anchor_meta=anchor_meta,
|
|
@@ -1128,7 +1117,7 @@ class RunManager:
|
|
|
1128
1117
|
message["id"],
|
|
1129
1118
|
source_meta,
|
|
1130
1119
|
meta,
|
|
1131
|
-
|
|
1120
|
+
applied,
|
|
1132
1121
|
run_id=run_id,
|
|
1133
1122
|
anchor=anchor,
|
|
1134
1123
|
anchor_meta=anchor_meta,
|
|
@@ -1137,14 +1126,14 @@ class RunManager:
|
|
|
1137
1126
|
)
|
|
1138
1127
|
|
|
1139
1128
|
async def enqueue(
|
|
1140
|
-
self, params: Any, *, meta: Any = None,
|
|
1129
|
+
self, params: Any, *, meta: Any = None, applied: Callable[[], None]
|
|
1141
1130
|
) -> Any:
|
|
1142
|
-
return await self._send("queue", params, meta,
|
|
1131
|
+
return await self._send("queue", params, meta, applied)
|
|
1143
1132
|
|
|
1144
1133
|
async def steer(
|
|
1145
|
-
self, params: Any, *, meta: Any = None,
|
|
1134
|
+
self, params: Any, *, meta: Any = None, applied: Callable[[], None]
|
|
1146
1135
|
) -> Any:
|
|
1147
|
-
return await self._send("steerQueue", params, meta,
|
|
1136
|
+
return await self._send("steerQueue", params, meta, applied)
|
|
1148
1137
|
|
|
1149
1138
|
async def dequeue(self, params: Any) -> None:
|
|
1150
1139
|
message_id = params.get("messageId") if isinstance(params, dict) else None
|
|
@@ -1163,7 +1152,7 @@ class RunManager:
|
|
|
1163
1152
|
raise _reject("capability-missing", "the rewind capability is not enabled")
|
|
1164
1153
|
|
|
1165
1154
|
async def edit(
|
|
1166
|
-
self, params: Any, *, meta: Any = None,
|
|
1155
|
+
self, params: Any, *, meta: Any = None, applied: Callable[[], None]
|
|
1167
1156
|
) -> Any:
|
|
1168
1157
|
self._check_rewind_gate("run/edit")
|
|
1169
1158
|
run_id = _run_id_of(params)
|
|
@@ -1187,11 +1176,11 @@ class RunManager:
|
|
|
1187
1176
|
):
|
|
1188
1177
|
raise _reject("duplicate-id", f"message id {message['id']} is already used")
|
|
1189
1178
|
return await self._stage(
|
|
1190
|
-
_Edit(source_id, source_meta, message, meta,
|
|
1179
|
+
_Edit(source_id, source_meta, message, meta, applied, run_id)
|
|
1191
1180
|
)
|
|
1192
1181
|
|
|
1193
1182
|
async def reload(
|
|
1194
|
-
self, params: Any, *, meta: Any = None,
|
|
1183
|
+
self, params: Any, *, meta: Any = None, applied: Callable[[], None]
|
|
1195
1184
|
) -> Any:
|
|
1196
1185
|
self._check_rewind_gate("run/reload")
|
|
1197
1186
|
run_id = _run_id_of(params)
|
|
@@ -1215,9 +1204,9 @@ class RunManager:
|
|
|
1215
1204
|
"capability-missing",
|
|
1216
1205
|
"the assistant-continuation capability is not enabled",
|
|
1217
1206
|
)
|
|
1218
|
-
return await self._stage(_Reload(source_meta, meta,
|
|
1207
|
+
return await self._stage(_Reload(source_meta, meta, applied, run_id))
|
|
1219
1208
|
|
|
1220
|
-
async def stop(self, params: Any = None, *,
|
|
1209
|
+
async def stop(self, params: Any = None, *, applied: Callable[[], None]) -> Any:
|
|
1221
1210
|
if params is not None and not isinstance(params, dict):
|
|
1222
1211
|
raise _reject("invalid-message", "params must be an object")
|
|
1223
1212
|
reason = (params or {}).get("reason", _ABSENT)
|
|
@@ -1235,7 +1224,7 @@ class RunManager:
|
|
|
1235
1224
|
self._stop_reason = reason
|
|
1236
1225
|
entry = _Stop()
|
|
1237
1226
|
self._intake.append(entry)
|
|
1238
|
-
|
|
1227
|
+
applied()
|
|
1239
1228
|
self._schedule(self._drain)
|
|
1240
1229
|
return await entry.future
|
|
1241
1230
|
|
|
@@ -1285,7 +1274,7 @@ class RunManager:
|
|
|
1285
1274
|
response = self._validated_response(request["type"], params["response"])
|
|
1286
1275
|
return await self._stage(_Input(request_id, response, meta))
|
|
1287
1276
|
|
|
1288
|
-
async def continue_run(self, *, meta: Any = None,
|
|
1277
|
+
async def continue_run(self, *, meta: Any = None, applied: Callable[[], None]) -> Any:
|
|
1289
1278
|
status = self._status()
|
|
1290
1279
|
if status not in ("error", "stopped"):
|
|
1291
1280
|
raise _reject("wrong-state", f"run/continue is rejected in {status}")
|
|
@@ -1298,7 +1287,7 @@ class RunManager:
|
|
|
1298
1287
|
)
|
|
1299
1288
|
entry = _Continue(meta)
|
|
1300
1289
|
self._intake.append(entry)
|
|
1301
|
-
|
|
1290
|
+
applied()
|
|
1302
1291
|
self._schedule(self._drain)
|
|
1303
1292
|
return await entry.future
|
|
1304
1293
|
|
|
@@ -1351,12 +1340,12 @@ class RunManager:
|
|
|
1351
1340
|
_ctx: "RunManager.RunContext"
|
|
1352
1341
|
|
|
1353
1342
|
def take(self) -> tuple[dict[str, Any], ...]:
|
|
1354
|
-
"""Drain the steer lane into a fresh dispatch record; requires the current batch to be
|
|
1343
|
+
"""Drain the steer lane into a fresh dispatch record; requires the current batch to be applied."""
|
|
1355
1344
|
self._ctx._ensure_active()
|
|
1356
1345
|
manager = self._ctx._manager
|
|
1357
1346
|
entry = manager._entry()
|
|
1358
1347
|
if plain(entry.get("dispatching")) is not None:
|
|
1359
|
-
raise RuntimeError("take before the current batch is
|
|
1348
|
+
raise RuntimeError("take before the current batch is applied")
|
|
1360
1349
|
items = manager._lane_items("steerQueue")
|
|
1361
1350
|
if not items:
|
|
1362
1351
|
return ()
|
|
@@ -1421,10 +1410,10 @@ class RunManager:
|
|
|
1421
1410
|
if self._manager._ctx is not self:
|
|
1422
1411
|
raise RuntimeError("this run has already settled")
|
|
1423
1412
|
|
|
1424
|
-
def
|
|
1425
|
-
"""
|
|
1413
|
+
def applied(self) -> None:
|
|
1414
|
+
"""Applied the current batch: clears the dispatch record."""
|
|
1426
1415
|
self._ensure_active()
|
|
1427
|
-
self._manager.
|
|
1416
|
+
self._manager._applied()
|
|
1428
1417
|
|
|
1429
1418
|
def set_recovery_state(self, value: Any) -> None:
|
|
1430
1419
|
self._ensure_active()
|
|
@@ -16,8 +16,8 @@ class Call:
|
|
|
16
16
|
ctx: RunManager.RunContext
|
|
17
17
|
outcome: "asyncio.Future[Any]"
|
|
18
18
|
|
|
19
|
-
def
|
|
20
|
-
self.ctx.
|
|
19
|
+
def applied(self) -> None:
|
|
20
|
+
self.ctx.applied()
|
|
21
21
|
|
|
22
22
|
def finish(self, outcome: Any) -> None:
|
|
23
23
|
self.outcome.set_result(outcome)
|
|
@@ -87,11 +87,11 @@ def make_host(
|
|
|
87
87
|
|
|
88
88
|
@command("run/enqueue")
|
|
89
89
|
async def run_enqueue(self, params, *, ctx):
|
|
90
|
-
return await self.runs.enqueue(params, meta=_meta(params),
|
|
90
|
+
return await self.runs.enqueue(params, meta=_meta(params), applied=ctx.applied)
|
|
91
91
|
|
|
92
92
|
@command("run/steer")
|
|
93
93
|
async def run_steer(self, params, *, ctx):
|
|
94
|
-
return await self.runs.steer(params, meta=_meta(params),
|
|
94
|
+
return await self.runs.steer(params, meta=_meta(params), applied=ctx.applied)
|
|
95
95
|
|
|
96
96
|
@command("run/dequeue")
|
|
97
97
|
async def run_dequeue(self, params):
|
|
@@ -99,19 +99,19 @@ def make_host(
|
|
|
99
99
|
|
|
100
100
|
@command("run/edit")
|
|
101
101
|
async def run_edit(self, params, *, ctx):
|
|
102
|
-
return await self.runs.edit(params, meta=_meta(params),
|
|
102
|
+
return await self.runs.edit(params, meta=_meta(params), applied=ctx.applied)
|
|
103
103
|
|
|
104
104
|
@command("run/reload")
|
|
105
105
|
async def run_reload(self, params, *, ctx):
|
|
106
|
-
return await self.runs.reload(params, meta=_meta(params),
|
|
106
|
+
return await self.runs.reload(params, meta=_meta(params), applied=ctx.applied)
|
|
107
107
|
|
|
108
108
|
@command("run/stop")
|
|
109
109
|
async def run_stop(self, params=None, *, ctx):
|
|
110
|
-
return await self.runs.stop(params,
|
|
110
|
+
return await self.runs.stop(params, applied=ctx.applied)
|
|
111
111
|
|
|
112
112
|
@command("run/continue")
|
|
113
113
|
async def run_continue(self, params=None, *, ctx):
|
|
114
|
-
return await self.runs.continue_run(meta=_meta(params),
|
|
114
|
+
return await self.runs.continue_run(meta=_meta(params), applied=ctx.applied)
|
|
115
115
|
|
|
116
116
|
@command("run/input")
|
|
117
117
|
async def run_input(self, params):
|
|
@@ -205,7 +205,7 @@ class RunDriver:
|
|
|
205
205
|
return await self.res(self._seq)
|
|
206
206
|
|
|
207
207
|
async def res(self, seq: int, *, terminal: bool = True) -> dict[str, Any]:
|
|
208
|
-
# A payload-less accept rides only the ack; rejections and late
|
|
208
|
+
# A payload-less accept rides only the legacy ack; rejections and late
|
|
209
209
|
# settles ride res.
|
|
210
210
|
while True:
|
|
211
211
|
entries = self._res.get(seq, [])
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
"""Contract:
|
|
1
|
+
"""Contract: applied rides visibility. A send's covering applied only rides an
|
|
2
2
|
envelope whose state carries the command's effect — the item in a published
|
|
3
3
|
queue lane or in ``dispatching``. An idle send's insert lands in
|
|
4
|
-
``dispatching`` directly, so its
|
|
5
|
-
lane never holds the item; a rewind's
|
|
4
|
+
``dispatching`` directly, so its applied rides the dispatch envelope and the queue
|
|
5
|
+
lane never holds the item; a rewind's applied rides its dispatch the same way."""
|
|
6
6
|
|
|
7
7
|
import copy
|
|
8
8
|
|
|
@@ -35,7 +35,7 @@ def assert_never_queued(drv, message_id):
|
|
|
35
35
|
assert message_id not in queue_ids(state, "queue")
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
async def
|
|
38
|
+
async def test_idle_send_applies_with_its_dispatching_insert():
|
|
39
39
|
script = Script()
|
|
40
40
|
async with run_host(script) as (drv, host):
|
|
41
41
|
seq = drv.post("run/enqueue", add("m1"))
|
|
@@ -44,23 +44,23 @@ async def test_idle_send_acks_with_its_dispatching_insert():
|
|
|
44
44
|
assert dispatching_ids(covered_state(drv, seq)) == ["m1"]
|
|
45
45
|
assert_never_queued(drv, "m1")
|
|
46
46
|
host.live.state["messages"] = [msg("m1")]
|
|
47
|
-
call.
|
|
47
|
+
call.applied()
|
|
48
48
|
call.finish(RunManager.Complete())
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
async def
|
|
51
|
+
async def test_parked_send_applies_with_its_queue_insert():
|
|
52
52
|
script = Script()
|
|
53
53
|
async with run_host(script) as (drv, host):
|
|
54
54
|
drv.post("run/enqueue", add("m1"))
|
|
55
55
|
call = await script.next_call()
|
|
56
|
-
call.
|
|
56
|
+
call.applied()
|
|
57
57
|
res = await drv.command("run/enqueue", add("m2"))
|
|
58
58
|
assert res["type"] == "accepted"
|
|
59
59
|
assert "m2" in queue_ids(covered_state(drv, res["seq"]), "queue")
|
|
60
60
|
call.finish(RunManager.Complete())
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
async def
|
|
63
|
+
async def test_unapplied_run_end_consumes_the_direct_dispatch():
|
|
64
64
|
script = Script()
|
|
65
65
|
async with run_host(script) as (drv, host):
|
|
66
66
|
seq = drv.post("run/enqueue", add("m1"))
|
|
@@ -72,7 +72,7 @@ async def test_unacked_run_end_consumes_the_direct_dispatch():
|
|
|
72
72
|
assert queue_ids(drv.replica, "queue") == []
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
async def
|
|
75
|
+
async def test_unapplied_run_end_consumes_the_direct_steer_send():
|
|
76
76
|
script = Script()
|
|
77
77
|
async with run_host(script) as (drv, host):
|
|
78
78
|
seq = drv.post("run/steer", add("s1"))
|
|
@@ -85,7 +85,7 @@ async def test_unacked_run_end_consumes_the_direct_steer_send():
|
|
|
85
85
|
assert queue_ids(drv.replica, "queue") == []
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
async def
|
|
88
|
+
async def test_rewind_edit_applies_with_its_dispatch():
|
|
89
89
|
script = Script()
|
|
90
90
|
script.thread["u1"] = {"parentId": None, "role": "user", "isLeaf": True}
|
|
91
91
|
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
@@ -95,6 +95,6 @@ async def test_rewind_edit_acks_with_its_dispatch():
|
|
|
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")]
|
|
98
|
-
rerun.
|
|
98
|
+
rerun.applied()
|
|
99
99
|
rerun.finish(RunManager.Complete())
|
|
100
100
|
assert (await drv.res(seq))["type"] == "accepted"
|
|
@@ -25,7 +25,7 @@ async def test_multi_steer_batch_places_all_and_dispatches_once():
|
|
|
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") == []
|
|
28
|
-
call.
|
|
28
|
+
call.applied()
|
|
29
29
|
call.finish(RunManager.Complete())
|
|
30
30
|
for offset in range(3):
|
|
31
31
|
assert (await drv.res(first + offset))["type"] == "accepted"
|
|
@@ -45,7 +45,7 @@ async def test_same_tick_sends_dispatch_as_one_run():
|
|
|
45
45
|
merged = await drv.res(first + 1)
|
|
46
46
|
assert merged["type"] == "accepted"
|
|
47
47
|
assert merged["payload"] == {"runId": mint["runId"]}
|
|
48
|
-
call.
|
|
48
|
+
call.applied()
|
|
49
49
|
call.finish(RunManager.Complete())
|
|
50
50
|
|
|
51
51
|
|
|
@@ -54,35 +54,29 @@ async def test_steer_and_stop_in_one_batch_nets_to_stop():
|
|
|
54
54
|
async with run_host(script) as (drv, host):
|
|
55
55
|
drv.post("run/enqueue", add("m1"))
|
|
56
56
|
call = await script.next_call()
|
|
57
|
-
call.
|
|
57
|
+
call.applied()
|
|
58
58
|
first = drv.batch(
|
|
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
|
|
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
|
|
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
|
-
call.
|
|
77
|
-
|
|
78
|
-
assert
|
|
79
|
-
|
|
80
|
-
await asyncio.sleep(0)
|
|
81
|
-
assert all(
|
|
82
|
-
rsp["type"] == "pending" for rsp in drv._res.get(pending["seq"], [])
|
|
83
|
-
)
|
|
75
|
+
call.applied()
|
|
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
|
|
|
@@ -110,10 +104,10 @@ async def test_handler_tail_staging_decides_in_its_own_envelope():
|
|
|
110
104
|
)
|
|
111
105
|
)
|
|
112
106
|
# The decide ran outside the batch segment: its ops flush in an
|
|
113
|
-
# envelope of their own, carrying the send's covering
|
|
107
|
+
# envelope of their own, carrying the send's covering applied.
|
|
114
108
|
assert dispatch_env.get("ack", 0) >= seq
|
|
115
109
|
assert (await drv.res(seq))["type"] == "accepted"
|
|
116
|
-
call.
|
|
110
|
+
call.applied()
|
|
117
111
|
|
|
118
112
|
|
|
119
113
|
async def test_rewind_outranks_send_and_continue_in_one_batch():
|
|
@@ -135,7 +129,7 @@ async def test_rewind_outranks_send_and_continue_in_one_batch():
|
|
|
135
129
|
"isLeaf": True,
|
|
136
130
|
"onActiveBranch": True,
|
|
137
131
|
}
|
|
138
|
-
call.
|
|
132
|
+
call.applied()
|
|
139
133
|
call.fail(RuntimeError("boom"))
|
|
140
134
|
await drv.wait_status("error")
|
|
141
135
|
first = drv.batch(
|
|
@@ -153,7 +147,7 @@ async def test_rewind_outranks_send_and_continue_in_one_batch():
|
|
|
153
147
|
assert (await drv.res(first + 1))["type"] == "accepted"
|
|
154
148
|
assert (await drv.res(first + 2))["type"] == "accepted"
|
|
155
149
|
assert queue_ids(drv.replica, "queue") == ["q1"]
|
|
156
|
-
rerun.
|
|
150
|
+
rerun.applied()
|
|
157
151
|
rerun.finish(RunManager.Complete())
|
|
158
152
|
|
|
159
153
|
|
|
@@ -162,7 +156,7 @@ async def test_send_outranks_the_completing_input_answer():
|
|
|
162
156
|
async with run_host(script) as (drv, host):
|
|
163
157
|
drv.post("run/enqueue", add("m1"))
|
|
164
158
|
call = await script.next_call()
|
|
165
|
-
call.
|
|
159
|
+
call.applied()
|
|
166
160
|
request = {"type": "tool-call", "id": "r1", "toolCallId": "tc1"}
|
|
167
161
|
call.finish(RunManager.InputRequired((request,)))
|
|
168
162
|
await drv.wait_status("input-required")
|
|
@@ -179,6 +173,6 @@ async def test_send_outranks_the_completing_input_answer():
|
|
|
179
173
|
script.no_call()
|
|
180
174
|
assert (await drv.res(first))["type"] == "accepted"
|
|
181
175
|
assert (await drv.res(first + 1))["type"] == "accepted"
|
|
182
|
-
run.
|
|
176
|
+
run.applied()
|
|
183
177
|
run.finish(RunManager.Complete())
|
|
184
178
|
await drv.wait_status("ready")
|