harness-sdk-python 0.13.2__tar.gz → 0.14.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/PKG-INFO +3 -3
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/examples/runs_app.py +7 -7
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/pyproject.toml +2 -2
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/src/harness_sdk/run_manager.py +69 -47
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/run_helpers.py +9 -9
- harness_sdk_python-0.13.2/tests/test_ack_visibility.py → harness_sdk_python-0.14.0/tests/test_applied_visibility.py +13 -13
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_batches.py +10 -10
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_branch_anchor.py +10 -10
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_dispatching.py +22 -22
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_edit_dispatched.py +8 -8
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_edit_reload.py +6 -6
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_enqueue.py +18 -18
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_input_required.py +20 -20
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_meta.py +16 -16
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_outcomes.py +9 -9
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_placement.py +1 -1
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_prepare_hooks.py +4 -4
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_restore.py +4 -4
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_rewind_during_run.py +6 -6
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_settle.py +39 -34
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_steer.py +11 -11
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_stop_continue.py +64 -22
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_stopping_window.py +16 -8
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/.gitignore +0 -0
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/README.md +0 -0
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/examples/__init__.py +0 -0
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/src/harness_sdk/linear_thread.py +0 -0
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.14.0}/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.
|
|
3
|
+
Version: 0.14.0
|
|
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.
|
|
3
|
+
version = "0.14.0"
|
|
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,16 +10,22 @@ 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
|
|
13
|
+
initiator applies and settles accepted with its state-visible application:
|
|
14
14
|
the verdict certifies the admission write — sends with their insert (lane
|
|
15
15
|
or ``dispatching``), mid-run edit/reload with the stopping window,
|
|
16
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
|
-
|
|
22
|
-
|
|
20
|
+
``ctx.applied()`` confirms it; ``steering.take()`` may refill it — at most one
|
|
21
|
+
unconfirmed batch ever exists. Settle consumes a confirmed batch; a batch
|
|
22
|
+
still unconfirmed at settle returns to the front of its lanes. ``Complete``
|
|
23
|
+
with an unconfirmed batch settles as an error. Steer entries still
|
|
24
|
+
undelivered in the lane stay queued for the next run.
|
|
25
|
+
|
|
26
|
+
``entry["epoch"]`` counts the entry's dispatches; a ``run/stop`` carrying an
|
|
27
|
+
``epoch`` other than the live one rejects, so a replayed stop never ends a
|
|
28
|
+
later leg of the same ``runId``.
|
|
23
29
|
|
|
24
30
|
A mid-run stop or rewind opens a stopping window on the entry:
|
|
25
31
|
``stopping: {reason}`` plus, for rewinds, the staged dispatch as
|
|
@@ -87,7 +93,7 @@ class _Send:
|
|
|
87
93
|
message_id: str
|
|
88
94
|
source_meta: dict[str, Any] | None
|
|
89
95
|
meta: Any
|
|
90
|
-
|
|
96
|
+
applied: Callable[[], None]
|
|
91
97
|
run_id: str = ""
|
|
92
98
|
anchor: Any = _ABSENT
|
|
93
99
|
anchor_meta: dict[str, Any] | None = None
|
|
@@ -107,7 +113,7 @@ class _Edit:
|
|
|
107
113
|
source_meta: dict[str, Any]
|
|
108
114
|
message: dict[str, Any]
|
|
109
115
|
meta: Any
|
|
110
|
-
|
|
116
|
+
applied: Callable[[], None]
|
|
111
117
|
run_id: str
|
|
112
118
|
future: "asyncio.Future[Any]" = field(default_factory=_future)
|
|
113
119
|
|
|
@@ -116,7 +122,7 @@ class _Edit:
|
|
|
116
122
|
class _Reload:
|
|
117
123
|
source_meta: dict[str, Any]
|
|
118
124
|
meta: Any
|
|
119
|
-
|
|
125
|
+
applied: Callable[[], None]
|
|
120
126
|
run_id: str
|
|
121
127
|
future: "asyncio.Future[Any]" = field(default_factory=_future)
|
|
122
128
|
|
|
@@ -145,11 +151,11 @@ class _Rewind:
|
|
|
145
151
|
type: str
|
|
146
152
|
messages: list[dict[str, Any]]
|
|
147
153
|
rollback_to: Any
|
|
148
|
-
|
|
154
|
+
applied: Callable[[], None]
|
|
149
155
|
future: "asyncio.Future[Any]"
|
|
150
156
|
run_id: str
|
|
151
157
|
root_meta: Any = None
|
|
152
|
-
|
|
158
|
+
applied_sent: bool = False
|
|
153
159
|
|
|
154
160
|
|
|
155
161
|
@dataclass
|
|
@@ -373,8 +379,8 @@ class RunManager:
|
|
|
373
379
|
rollback_to=rewind.rollback_to,
|
|
374
380
|
root_meta=rewind.root_meta,
|
|
375
381
|
)
|
|
376
|
-
if not rewind.
|
|
377
|
-
rewind.
|
|
382
|
+
if not rewind.applied_sent:
|
|
383
|
+
rewind.applied()
|
|
378
384
|
elif outcome is not None:
|
|
379
385
|
self._settle_outcome(outcome)
|
|
380
386
|
else:
|
|
@@ -384,8 +390,8 @@ class RunManager:
|
|
|
384
390
|
):
|
|
385
391
|
assert self._ctx is not None
|
|
386
392
|
self._ctx.stop_requested.set()
|
|
387
|
-
for future,
|
|
388
|
-
|
|
393
|
+
for future, applied, result in fx.staged_sends:
|
|
394
|
+
applied()
|
|
389
395
|
if not future.done():
|
|
390
396
|
future.set_result(result)
|
|
391
397
|
for future in fx.continues:
|
|
@@ -534,6 +540,7 @@ class RunManager:
|
|
|
534
540
|
self._dispatch_record = dict(record)
|
|
535
541
|
self._stop_reason = None
|
|
536
542
|
entry = self._ensure_entry()
|
|
543
|
+
entry["epoch"] = (plain(entry.get("epoch")) or 0) + 1
|
|
537
544
|
entry["dispatching"] = record
|
|
538
545
|
entry.pop("error", None)
|
|
539
546
|
if messages:
|
|
@@ -570,6 +577,8 @@ class RunManager:
|
|
|
570
577
|
"run must return a RunManager outcome, got "
|
|
571
578
|
f"{type(outcome).__name__}"
|
|
572
579
|
)
|
|
580
|
+
if isinstance(outcome, RunManager.Complete) and self._dispatching:
|
|
581
|
+
raise RuntimeError("run returned Complete with an unconfirmed batch")
|
|
573
582
|
except asyncio.CancelledError:
|
|
574
583
|
self._settle(ctx)
|
|
575
584
|
self._set_status("stopped")
|
|
@@ -593,19 +602,25 @@ class RunManager:
|
|
|
593
602
|
self._ctx = None
|
|
594
603
|
self._task = None
|
|
595
604
|
self._dispatch_record = None
|
|
596
|
-
self._dispatching = []
|
|
597
605
|
entry = self._ensure_entry()
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
606
|
+
if self._dispatching:
|
|
607
|
+
self._unstage_dispatching()
|
|
608
|
+
else:
|
|
609
|
+
record = plain(entry.get("dispatching"))
|
|
610
|
+
if (
|
|
611
|
+
record is not None
|
|
612
|
+
and record["trigger"] != "steer"
|
|
613
|
+
and record["messages"]
|
|
614
|
+
):
|
|
615
|
+
self._dispatched_ids = ()
|
|
616
|
+
entry.pop("dispatching", None)
|
|
602
617
|
entry.pop("stopping", None)
|
|
603
618
|
|
|
604
|
-
def
|
|
619
|
+
def _applied(self) -> None:
|
|
605
620
|
entry = self._entry()
|
|
606
621
|
record = plain(entry.get("dispatching"))
|
|
607
622
|
if record is None:
|
|
608
|
-
raise RuntimeError("
|
|
623
|
+
raise RuntimeError("applied() with no unapplied batch")
|
|
609
624
|
self._dispatching = []
|
|
610
625
|
entry.pop("dispatching", None)
|
|
611
626
|
|
|
@@ -691,8 +706,8 @@ class RunManager:
|
|
|
691
706
|
rollback_to=rewind.rollback_to,
|
|
692
707
|
root_meta=rewind.root_meta,
|
|
693
708
|
)
|
|
694
|
-
rewind.
|
|
695
|
-
rewind.
|
|
709
|
+
rewind.applied()
|
|
710
|
+
rewind.applied_sent = True
|
|
696
711
|
if not rewind.future.done():
|
|
697
712
|
rewind.future.set_result(None)
|
|
698
713
|
|
|
@@ -941,7 +956,7 @@ class RunManager:
|
|
|
941
956
|
if live_run_id is not None and e.anchor is not _ABSENT
|
|
942
957
|
else None
|
|
943
958
|
)
|
|
944
|
-
fx.staged_sends.append((e.future, e.
|
|
959
|
+
fx.staged_sends.append((e.future, e.applied, result))
|
|
945
960
|
return _PARKED
|
|
946
961
|
|
|
947
962
|
def _apply_move(self, e: _Send, fx: _Effects) -> Any:
|
|
@@ -981,7 +996,7 @@ class RunManager:
|
|
|
981
996
|
"message-edit",
|
|
982
997
|
[self._stamped(e.message, e.meta)],
|
|
983
998
|
e.source_meta["parentId"],
|
|
984
|
-
e.
|
|
999
|
+
e.applied,
|
|
985
1000
|
e.future,
|
|
986
1001
|
e.run_id,
|
|
987
1002
|
)
|
|
@@ -1036,7 +1051,7 @@ class RunManager:
|
|
|
1036
1051
|
"message-edit",
|
|
1037
1052
|
[self._stamped(e.message, e.meta)],
|
|
1038
1053
|
e.source_meta["parentId"],
|
|
1039
|
-
e.
|
|
1054
|
+
e.applied,
|
|
1040
1055
|
e.future,
|
|
1041
1056
|
e.run_id,
|
|
1042
1057
|
)
|
|
@@ -1049,7 +1064,7 @@ class RunManager:
|
|
|
1049
1064
|
"message-reload",
|
|
1050
1065
|
[],
|
|
1051
1066
|
e.source_meta["parentId"],
|
|
1052
|
-
e.
|
|
1067
|
+
e.applied,
|
|
1053
1068
|
e.future,
|
|
1054
1069
|
e.run_id,
|
|
1055
1070
|
root_meta=e.meta,
|
|
@@ -1064,7 +1079,7 @@ class RunManager:
|
|
|
1064
1079
|
lane: str,
|
|
1065
1080
|
params: Any,
|
|
1066
1081
|
meta: Any,
|
|
1067
|
-
|
|
1082
|
+
applied: Callable[[], None],
|
|
1068
1083
|
) -> Any:
|
|
1069
1084
|
if not isinstance(params, dict):
|
|
1070
1085
|
raise _reject("invalid-message", "params must be an object")
|
|
@@ -1100,7 +1115,7 @@ class RunManager:
|
|
|
1100
1115
|
message_id,
|
|
1101
1116
|
None,
|
|
1102
1117
|
None,
|
|
1103
|
-
|
|
1118
|
+
applied,
|
|
1104
1119
|
run_id=run_id,
|
|
1105
1120
|
anchor=anchor,
|
|
1106
1121
|
anchor_meta=anchor_meta,
|
|
@@ -1117,7 +1132,7 @@ class RunManager:
|
|
|
1117
1132
|
message["id"],
|
|
1118
1133
|
source_meta,
|
|
1119
1134
|
meta,
|
|
1120
|
-
|
|
1135
|
+
applied,
|
|
1121
1136
|
run_id=run_id,
|
|
1122
1137
|
anchor=anchor,
|
|
1123
1138
|
anchor_meta=anchor_meta,
|
|
@@ -1126,14 +1141,14 @@ class RunManager:
|
|
|
1126
1141
|
)
|
|
1127
1142
|
|
|
1128
1143
|
async def enqueue(
|
|
1129
|
-
self, params: Any, *, meta: Any = None,
|
|
1144
|
+
self, params: Any, *, meta: Any = None, applied: Callable[[], None]
|
|
1130
1145
|
) -> Any:
|
|
1131
|
-
return await self._send("queue", params, meta,
|
|
1146
|
+
return await self._send("queue", params, meta, applied)
|
|
1132
1147
|
|
|
1133
1148
|
async def steer(
|
|
1134
|
-
self, params: Any, *, meta: Any = None,
|
|
1149
|
+
self, params: Any, *, meta: Any = None, applied: Callable[[], None]
|
|
1135
1150
|
) -> Any:
|
|
1136
|
-
return await self._send("steerQueue", params, meta,
|
|
1151
|
+
return await self._send("steerQueue", params, meta, applied)
|
|
1137
1152
|
|
|
1138
1153
|
async def dequeue(self, params: Any) -> None:
|
|
1139
1154
|
message_id = params.get("messageId") if isinstance(params, dict) else None
|
|
@@ -1152,7 +1167,7 @@ class RunManager:
|
|
|
1152
1167
|
raise _reject("capability-missing", "the rewind capability is not enabled")
|
|
1153
1168
|
|
|
1154
1169
|
async def edit(
|
|
1155
|
-
self, params: Any, *, meta: Any = None,
|
|
1170
|
+
self, params: Any, *, meta: Any = None, applied: Callable[[], None]
|
|
1156
1171
|
) -> Any:
|
|
1157
1172
|
self._check_rewind_gate("run/edit")
|
|
1158
1173
|
run_id = _run_id_of(params)
|
|
@@ -1176,11 +1191,11 @@ class RunManager:
|
|
|
1176
1191
|
):
|
|
1177
1192
|
raise _reject("duplicate-id", f"message id {message['id']} is already used")
|
|
1178
1193
|
return await self._stage(
|
|
1179
|
-
_Edit(source_id, source_meta, message, meta,
|
|
1194
|
+
_Edit(source_id, source_meta, message, meta, applied, run_id)
|
|
1180
1195
|
)
|
|
1181
1196
|
|
|
1182
1197
|
async def reload(
|
|
1183
|
-
self, params: Any, *, meta: Any = None,
|
|
1198
|
+
self, params: Any, *, meta: Any = None, applied: Callable[[], None]
|
|
1184
1199
|
) -> Any:
|
|
1185
1200
|
self._check_rewind_gate("run/reload")
|
|
1186
1201
|
run_id = _run_id_of(params)
|
|
@@ -1204,14 +1219,17 @@ class RunManager:
|
|
|
1204
1219
|
"capability-missing",
|
|
1205
1220
|
"the assistant-continuation capability is not enabled",
|
|
1206
1221
|
)
|
|
1207
|
-
return await self._stage(_Reload(source_meta, meta,
|
|
1222
|
+
return await self._stage(_Reload(source_meta, meta, applied, run_id))
|
|
1208
1223
|
|
|
1209
|
-
async def stop(self, params: Any = None, *,
|
|
1224
|
+
async def stop(self, params: Any = None, *, applied: Callable[[], None]) -> Any:
|
|
1210
1225
|
if params is not None and not isinstance(params, dict):
|
|
1211
1226
|
raise _reject("invalid-message", "params must be an object")
|
|
1212
1227
|
reason = (params or {}).get("reason", _ABSENT)
|
|
1213
1228
|
if reason is not _ABSENT and not isinstance(reason, str):
|
|
1214
1229
|
raise _reject("invalid-message", "reason must be a string")
|
|
1230
|
+
epoch = (params or {}).get("epoch", _ABSENT)
|
|
1231
|
+
if epoch is not _ABSENT and (isinstance(epoch, bool) or not isinstance(epoch, int)):
|
|
1232
|
+
raise _reject("invalid-message", "epoch must be an integer")
|
|
1215
1233
|
status = self._status()
|
|
1216
1234
|
if status != "running":
|
|
1217
1235
|
raise _reject("wrong-state", f"run/stop is rejected in {status}")
|
|
@@ -1220,11 +1238,15 @@ class RunManager:
|
|
|
1220
1238
|
raise _reject(
|
|
1221
1239
|
"wrong-state", f"runId {run_id!r} does not name the live run"
|
|
1222
1240
|
)
|
|
1241
|
+
if epoch is not _ABSENT and epoch != plain(self._entry()["epoch"]):
|
|
1242
|
+
raise _reject(
|
|
1243
|
+
"wrong-state", f"epoch {epoch} does not name the live dispatch"
|
|
1244
|
+
)
|
|
1223
1245
|
if reason is not _ABSENT:
|
|
1224
1246
|
self._stop_reason = reason
|
|
1225
1247
|
entry = _Stop()
|
|
1226
1248
|
self._intake.append(entry)
|
|
1227
|
-
|
|
1249
|
+
applied()
|
|
1228
1250
|
self._schedule(self._drain)
|
|
1229
1251
|
return await entry.future
|
|
1230
1252
|
|
|
@@ -1274,7 +1296,7 @@ class RunManager:
|
|
|
1274
1296
|
response = self._validated_response(request["type"], params["response"])
|
|
1275
1297
|
return await self._stage(_Input(request_id, response, meta))
|
|
1276
1298
|
|
|
1277
|
-
async def continue_run(self, *, meta: Any = None,
|
|
1299
|
+
async def continue_run(self, *, meta: Any = None, applied: Callable[[], None]) -> Any:
|
|
1278
1300
|
status = self._status()
|
|
1279
1301
|
if status not in ("error", "stopped"):
|
|
1280
1302
|
raise _reject("wrong-state", f"run/continue is rejected in {status}")
|
|
@@ -1287,7 +1309,7 @@ class RunManager:
|
|
|
1287
1309
|
)
|
|
1288
1310
|
entry = _Continue(meta)
|
|
1289
1311
|
self._intake.append(entry)
|
|
1290
|
-
|
|
1312
|
+
applied()
|
|
1291
1313
|
self._schedule(self._drain)
|
|
1292
1314
|
return await entry.future
|
|
1293
1315
|
|
|
@@ -1340,12 +1362,12 @@ class RunManager:
|
|
|
1340
1362
|
_ctx: "RunManager.RunContext"
|
|
1341
1363
|
|
|
1342
1364
|
def take(self) -> tuple[dict[str, Any], ...]:
|
|
1343
|
-
"""Drain the steer lane into a fresh dispatch record; requires the current batch to be
|
|
1365
|
+
"""Drain the steer lane into a fresh dispatch record; requires the current batch to be applied."""
|
|
1344
1366
|
self._ctx._ensure_active()
|
|
1345
1367
|
manager = self._ctx._manager
|
|
1346
1368
|
entry = manager._entry()
|
|
1347
1369
|
if plain(entry.get("dispatching")) is not None:
|
|
1348
|
-
raise RuntimeError("take before the current batch is
|
|
1370
|
+
raise RuntimeError("take before the current batch is applied")
|
|
1349
1371
|
items = manager._lane_items("steerQueue")
|
|
1350
1372
|
if not items:
|
|
1351
1373
|
return ()
|
|
@@ -1410,10 +1432,10 @@ class RunManager:
|
|
|
1410
1432
|
if self._manager._ctx is not self:
|
|
1411
1433
|
raise RuntimeError("this run has already settled")
|
|
1412
1434
|
|
|
1413
|
-
def
|
|
1414
|
-
"""
|
|
1435
|
+
def applied(self) -> None:
|
|
1436
|
+
"""Confirms the current batch durable: clears the dispatch record so settle consumes it."""
|
|
1415
1437
|
self._ensure_active()
|
|
1416
|
-
self._manager.
|
|
1438
|
+
self._manager._applied()
|
|
1417
1439
|
|
|
1418
1440
|
def set_recovery_state(self, value: Any) -> None:
|
|
1419
1441
|
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_unconfirmed_run_end_requeues_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"))
|
|
@@ -69,10 +69,10 @@ async def test_unacked_run_end_consumes_the_direct_dispatch():
|
|
|
69
69
|
assert dispatching_ids(covered_state(drv, seq)) == ["m1"]
|
|
70
70
|
call.fail(RuntimeError("boom"))
|
|
71
71
|
await drv.wait_status("error")
|
|
72
|
-
assert queue_ids(drv.replica, "queue") == []
|
|
72
|
+
assert queue_ids(drv.replica, "queue") == ["m1"]
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
async def
|
|
75
|
+
async def test_unconfirmed_run_end_requeues_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"))
|
|
@@ -81,11 +81,11 @@ async def test_unacked_run_end_consumes_the_direct_steer_send():
|
|
|
81
81
|
assert dispatching_ids(covered_state(drv, seq)) == ["s1"]
|
|
82
82
|
call.fail(RuntimeError("boom"))
|
|
83
83
|
await drv.wait_status("error")
|
|
84
|
-
assert queue_ids(drv.replica, "steerQueue") == []
|
|
84
|
+
assert queue_ids(drv.replica, "steerQueue") == ["s1"]
|
|
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,7 +54,7 @@ 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
|
)
|
|
@@ -72,7 +72,7 @@ async def test_stop_settles_at_the_stopping_write():
|
|
|
72
72
|
async with run_host(script) as (drv, host):
|
|
73
73
|
drv.post("run/enqueue", add("m1"))
|
|
74
74
|
call = await script.next_call()
|
|
75
|
-
call.
|
|
75
|
+
call.applied()
|
|
76
76
|
settled = await drv.stop()
|
|
77
77
|
assert settled["type"] == "accepted"
|
|
78
78
|
assert drv.replica["status"] == "running"
|
|
@@ -104,10 +104,10 @@ async def test_handler_tail_staging_decides_in_its_own_envelope():
|
|
|
104
104
|
)
|
|
105
105
|
)
|
|
106
106
|
# The decide ran outside the batch segment: its ops flush in an
|
|
107
|
-
# envelope of their own, carrying the send's covering
|
|
107
|
+
# envelope of their own, carrying the send's covering applied.
|
|
108
108
|
assert dispatch_env.get("ack", 0) >= seq
|
|
109
109
|
assert (await drv.res(seq))["type"] == "accepted"
|
|
110
|
-
call.
|
|
110
|
+
call.applied()
|
|
111
111
|
|
|
112
112
|
|
|
113
113
|
async def test_rewind_outranks_send_and_continue_in_one_batch():
|
|
@@ -129,7 +129,7 @@ async def test_rewind_outranks_send_and_continue_in_one_batch():
|
|
|
129
129
|
"isLeaf": True,
|
|
130
130
|
"onActiveBranch": True,
|
|
131
131
|
}
|
|
132
|
-
call.
|
|
132
|
+
call.applied()
|
|
133
133
|
call.fail(RuntimeError("boom"))
|
|
134
134
|
await drv.wait_status("error")
|
|
135
135
|
first = drv.batch(
|
|
@@ -147,7 +147,7 @@ async def test_rewind_outranks_send_and_continue_in_one_batch():
|
|
|
147
147
|
assert (await drv.res(first + 1))["type"] == "accepted"
|
|
148
148
|
assert (await drv.res(first + 2))["type"] == "accepted"
|
|
149
149
|
assert queue_ids(drv.replica, "queue") == ["q1"]
|
|
150
|
-
rerun.
|
|
150
|
+
rerun.applied()
|
|
151
151
|
rerun.finish(RunManager.Complete())
|
|
152
152
|
|
|
153
153
|
|
|
@@ -156,7 +156,7 @@ async def test_send_outranks_the_completing_input_answer():
|
|
|
156
156
|
async with run_host(script) as (drv, host):
|
|
157
157
|
drv.post("run/enqueue", add("m1"))
|
|
158
158
|
call = await script.next_call()
|
|
159
|
-
call.
|
|
159
|
+
call.applied()
|
|
160
160
|
request = {"type": "tool-call", "id": "r1", "toolCallId": "tc1"}
|
|
161
161
|
call.finish(RunManager.InputRequired((request,)))
|
|
162
162
|
await drv.wait_status("input-required")
|
|
@@ -173,6 +173,6 @@ async def test_send_outranks_the_completing_input_answer():
|
|
|
173
173
|
script.no_call()
|
|
174
174
|
assert (await drv.res(first))["type"] == "accepted"
|
|
175
175
|
assert (await drv.res(first + 1))["type"] == "accepted"
|
|
176
|
-
run.
|
|
176
|
+
run.applied()
|
|
177
177
|
run.finish(RunManager.Complete())
|
|
178
178
|
await drv.wait_status("ready")
|