harness-sdk-python 0.13.2__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.2 → harness_sdk_python-0.13.3}/PKG-INFO +3 -3
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/examples/runs_app.py +7 -7
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/pyproject.toml +2 -2
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/src/harness_sdk/run_manager.py +41 -41
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/run_helpers.py +9 -9
- harness_sdk_python-0.13.2/tests/test_ack_visibility.py → harness_sdk_python-0.13.3/tests/test_applied_visibility.py +11 -11
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_batches.py +10 -10
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_branch_anchor.py +10 -10
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_dispatching.py +21 -21
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_edit_dispatched.py +8 -8
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_edit_reload.py +6 -6
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_enqueue.py +18 -18
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_input_required.py +20 -20
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_meta.py +16 -16
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_outcomes.py +9 -9
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_placement.py +1 -1
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_prepare_hooks.py +4 -4
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_restore.py +4 -4
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_rewind_during_run.py +6 -6
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_settle.py +14 -14
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_steer.py +11 -11
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_stop_continue.py +22 -22
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_stopping_window.py +7 -7
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/.gitignore +0 -0
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/README.md +0 -0
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/examples/__init__.py +0 -0
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/src/harness_sdk/linear_thread.py +0 -0
- {harness_sdk_python-0.13.2 → harness_sdk_python-0.13.3}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.13.2 → 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,15 +10,15 @@ 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
|
-
|
|
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:
|
|
@@ -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
|
|
@@ -373,8 +373,8 @@ class RunManager:
|
|
|
373
373
|
rollback_to=rewind.rollback_to,
|
|
374
374
|
root_meta=rewind.root_meta,
|
|
375
375
|
)
|
|
376
|
-
if not rewind.
|
|
377
|
-
rewind.
|
|
376
|
+
if not rewind.applied_sent:
|
|
377
|
+
rewind.applied()
|
|
378
378
|
elif outcome is not None:
|
|
379
379
|
self._settle_outcome(outcome)
|
|
380
380
|
else:
|
|
@@ -384,8 +384,8 @@ class RunManager:
|
|
|
384
384
|
):
|
|
385
385
|
assert self._ctx is not None
|
|
386
386
|
self._ctx.stop_requested.set()
|
|
387
|
-
for future,
|
|
388
|
-
|
|
387
|
+
for future, applied, result in fx.staged_sends:
|
|
388
|
+
applied()
|
|
389
389
|
if not future.done():
|
|
390
390
|
future.set_result(result)
|
|
391
391
|
for future in fx.continues:
|
|
@@ -601,11 +601,11 @@ class RunManager:
|
|
|
601
601
|
entry.pop("dispatching", None)
|
|
602
602
|
entry.pop("stopping", None)
|
|
603
603
|
|
|
604
|
-
def
|
|
604
|
+
def _applied(self) -> None:
|
|
605
605
|
entry = self._entry()
|
|
606
606
|
record = plain(entry.get("dispatching"))
|
|
607
607
|
if record is None:
|
|
608
|
-
raise RuntimeError("
|
|
608
|
+
raise RuntimeError("applied() with no unapplied batch")
|
|
609
609
|
self._dispatching = []
|
|
610
610
|
entry.pop("dispatching", None)
|
|
611
611
|
|
|
@@ -691,8 +691,8 @@ class RunManager:
|
|
|
691
691
|
rollback_to=rewind.rollback_to,
|
|
692
692
|
root_meta=rewind.root_meta,
|
|
693
693
|
)
|
|
694
|
-
rewind.
|
|
695
|
-
rewind.
|
|
694
|
+
rewind.applied()
|
|
695
|
+
rewind.applied_sent = True
|
|
696
696
|
if not rewind.future.done():
|
|
697
697
|
rewind.future.set_result(None)
|
|
698
698
|
|
|
@@ -941,7 +941,7 @@ class RunManager:
|
|
|
941
941
|
if live_run_id is not None and e.anchor is not _ABSENT
|
|
942
942
|
else None
|
|
943
943
|
)
|
|
944
|
-
fx.staged_sends.append((e.future, e.
|
|
944
|
+
fx.staged_sends.append((e.future, e.applied, result))
|
|
945
945
|
return _PARKED
|
|
946
946
|
|
|
947
947
|
def _apply_move(self, e: _Send, fx: _Effects) -> Any:
|
|
@@ -981,7 +981,7 @@ class RunManager:
|
|
|
981
981
|
"message-edit",
|
|
982
982
|
[self._stamped(e.message, e.meta)],
|
|
983
983
|
e.source_meta["parentId"],
|
|
984
|
-
e.
|
|
984
|
+
e.applied,
|
|
985
985
|
e.future,
|
|
986
986
|
e.run_id,
|
|
987
987
|
)
|
|
@@ -1036,7 +1036,7 @@ class RunManager:
|
|
|
1036
1036
|
"message-edit",
|
|
1037
1037
|
[self._stamped(e.message, e.meta)],
|
|
1038
1038
|
e.source_meta["parentId"],
|
|
1039
|
-
e.
|
|
1039
|
+
e.applied,
|
|
1040
1040
|
e.future,
|
|
1041
1041
|
e.run_id,
|
|
1042
1042
|
)
|
|
@@ -1049,7 +1049,7 @@ class RunManager:
|
|
|
1049
1049
|
"message-reload",
|
|
1050
1050
|
[],
|
|
1051
1051
|
e.source_meta["parentId"],
|
|
1052
|
-
e.
|
|
1052
|
+
e.applied,
|
|
1053
1053
|
e.future,
|
|
1054
1054
|
e.run_id,
|
|
1055
1055
|
root_meta=e.meta,
|
|
@@ -1064,7 +1064,7 @@ class RunManager:
|
|
|
1064
1064
|
lane: str,
|
|
1065
1065
|
params: Any,
|
|
1066
1066
|
meta: Any,
|
|
1067
|
-
|
|
1067
|
+
applied: Callable[[], None],
|
|
1068
1068
|
) -> Any:
|
|
1069
1069
|
if not isinstance(params, dict):
|
|
1070
1070
|
raise _reject("invalid-message", "params must be an object")
|
|
@@ -1100,7 +1100,7 @@ class RunManager:
|
|
|
1100
1100
|
message_id,
|
|
1101
1101
|
None,
|
|
1102
1102
|
None,
|
|
1103
|
-
|
|
1103
|
+
applied,
|
|
1104
1104
|
run_id=run_id,
|
|
1105
1105
|
anchor=anchor,
|
|
1106
1106
|
anchor_meta=anchor_meta,
|
|
@@ -1117,7 +1117,7 @@ class RunManager:
|
|
|
1117
1117
|
message["id"],
|
|
1118
1118
|
source_meta,
|
|
1119
1119
|
meta,
|
|
1120
|
-
|
|
1120
|
+
applied,
|
|
1121
1121
|
run_id=run_id,
|
|
1122
1122
|
anchor=anchor,
|
|
1123
1123
|
anchor_meta=anchor_meta,
|
|
@@ -1126,14 +1126,14 @@ class RunManager:
|
|
|
1126
1126
|
)
|
|
1127
1127
|
|
|
1128
1128
|
async def enqueue(
|
|
1129
|
-
self, params: Any, *, meta: Any = None,
|
|
1129
|
+
self, params: Any, *, meta: Any = None, applied: Callable[[], None]
|
|
1130
1130
|
) -> Any:
|
|
1131
|
-
return await self._send("queue", params, meta,
|
|
1131
|
+
return await self._send("queue", params, meta, applied)
|
|
1132
1132
|
|
|
1133
1133
|
async def steer(
|
|
1134
|
-
self, params: Any, *, meta: Any = None,
|
|
1134
|
+
self, params: Any, *, meta: Any = None, applied: Callable[[], None]
|
|
1135
1135
|
) -> Any:
|
|
1136
|
-
return await self._send("steerQueue", params, meta,
|
|
1136
|
+
return await self._send("steerQueue", params, meta, applied)
|
|
1137
1137
|
|
|
1138
1138
|
async def dequeue(self, params: Any) -> None:
|
|
1139
1139
|
message_id = params.get("messageId") if isinstance(params, dict) else None
|
|
@@ -1152,7 +1152,7 @@ class RunManager:
|
|
|
1152
1152
|
raise _reject("capability-missing", "the rewind capability is not enabled")
|
|
1153
1153
|
|
|
1154
1154
|
async def edit(
|
|
1155
|
-
self, params: Any, *, meta: Any = None,
|
|
1155
|
+
self, params: Any, *, meta: Any = None, applied: Callable[[], None]
|
|
1156
1156
|
) -> Any:
|
|
1157
1157
|
self._check_rewind_gate("run/edit")
|
|
1158
1158
|
run_id = _run_id_of(params)
|
|
@@ -1176,11 +1176,11 @@ class RunManager:
|
|
|
1176
1176
|
):
|
|
1177
1177
|
raise _reject("duplicate-id", f"message id {message['id']} is already used")
|
|
1178
1178
|
return await self._stage(
|
|
1179
|
-
_Edit(source_id, source_meta, message, meta,
|
|
1179
|
+
_Edit(source_id, source_meta, message, meta, applied, run_id)
|
|
1180
1180
|
)
|
|
1181
1181
|
|
|
1182
1182
|
async def reload(
|
|
1183
|
-
self, params: Any, *, meta: Any = None,
|
|
1183
|
+
self, params: Any, *, meta: Any = None, applied: Callable[[], None]
|
|
1184
1184
|
) -> Any:
|
|
1185
1185
|
self._check_rewind_gate("run/reload")
|
|
1186
1186
|
run_id = _run_id_of(params)
|
|
@@ -1204,9 +1204,9 @@ class RunManager:
|
|
|
1204
1204
|
"capability-missing",
|
|
1205
1205
|
"the assistant-continuation capability is not enabled",
|
|
1206
1206
|
)
|
|
1207
|
-
return await self._stage(_Reload(source_meta, meta,
|
|
1207
|
+
return await self._stage(_Reload(source_meta, meta, applied, run_id))
|
|
1208
1208
|
|
|
1209
|
-
async def stop(self, params: Any = None, *,
|
|
1209
|
+
async def stop(self, params: Any = None, *, applied: Callable[[], None]) -> Any:
|
|
1210
1210
|
if params is not None and not isinstance(params, dict):
|
|
1211
1211
|
raise _reject("invalid-message", "params must be an object")
|
|
1212
1212
|
reason = (params or {}).get("reason", _ABSENT)
|
|
@@ -1224,7 +1224,7 @@ class RunManager:
|
|
|
1224
1224
|
self._stop_reason = reason
|
|
1225
1225
|
entry = _Stop()
|
|
1226
1226
|
self._intake.append(entry)
|
|
1227
|
-
|
|
1227
|
+
applied()
|
|
1228
1228
|
self._schedule(self._drain)
|
|
1229
1229
|
return await entry.future
|
|
1230
1230
|
|
|
@@ -1274,7 +1274,7 @@ class RunManager:
|
|
|
1274
1274
|
response = self._validated_response(request["type"], params["response"])
|
|
1275
1275
|
return await self._stage(_Input(request_id, response, meta))
|
|
1276
1276
|
|
|
1277
|
-
async def continue_run(self, *, meta: Any = None,
|
|
1277
|
+
async def continue_run(self, *, meta: Any = None, applied: Callable[[], None]) -> Any:
|
|
1278
1278
|
status = self._status()
|
|
1279
1279
|
if status not in ("error", "stopped"):
|
|
1280
1280
|
raise _reject("wrong-state", f"run/continue is rejected in {status}")
|
|
@@ -1287,7 +1287,7 @@ class RunManager:
|
|
|
1287
1287
|
)
|
|
1288
1288
|
entry = _Continue(meta)
|
|
1289
1289
|
self._intake.append(entry)
|
|
1290
|
-
|
|
1290
|
+
applied()
|
|
1291
1291
|
self._schedule(self._drain)
|
|
1292
1292
|
return await entry.future
|
|
1293
1293
|
|
|
@@ -1340,12 +1340,12 @@ class RunManager:
|
|
|
1340
1340
|
_ctx: "RunManager.RunContext"
|
|
1341
1341
|
|
|
1342
1342
|
def take(self) -> tuple[dict[str, Any], ...]:
|
|
1343
|
-
"""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."""
|
|
1344
1344
|
self._ctx._ensure_active()
|
|
1345
1345
|
manager = self._ctx._manager
|
|
1346
1346
|
entry = manager._entry()
|
|
1347
1347
|
if plain(entry.get("dispatching")) is not None:
|
|
1348
|
-
raise RuntimeError("take before the current batch is
|
|
1348
|
+
raise RuntimeError("take before the current batch is applied")
|
|
1349
1349
|
items = manager._lane_items("steerQueue")
|
|
1350
1350
|
if not items:
|
|
1351
1351
|
return ()
|
|
@@ -1410,10 +1410,10 @@ class RunManager:
|
|
|
1410
1410
|
if self._manager._ctx is not self:
|
|
1411
1411
|
raise RuntimeError("this run has already settled")
|
|
1412
1412
|
|
|
1413
|
-
def
|
|
1414
|
-
"""
|
|
1413
|
+
def applied(self) -> None:
|
|
1414
|
+
"""Applied the current batch: clears the dispatch record."""
|
|
1415
1415
|
self._ensure_active()
|
|
1416
|
-
self._manager.
|
|
1416
|
+
self._manager._applied()
|
|
1417
1417
|
|
|
1418
1418
|
def set_recovery_state(self, value: Any) -> None:
|
|
1419
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,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")
|
|
@@ -48,7 +48,7 @@ async def test_anchorless_send_targets_the_live_run():
|
|
|
48
48
|
script = Script()
|
|
49
49
|
async with run_host(script) as (drv, host):
|
|
50
50
|
drv.post("run/enqueue", add("m1"))
|
|
51
|
-
(await script.next_call()).
|
|
51
|
+
(await script.next_call()).applied()
|
|
52
52
|
res = await drv.command(
|
|
53
53
|
"run/steer", {"runId": await drv.run_id(), "message": msg("s1")}, terminal=False
|
|
54
54
|
)
|
|
@@ -60,7 +60,7 @@ async def test_null_anchor_valid_only_on_an_empty_thread():
|
|
|
60
60
|
script = Script()
|
|
61
61
|
async with run_host(script) as (drv, host):
|
|
62
62
|
drv.post("run/enqueue", add("m1"))
|
|
63
|
-
(await script.next_call()).
|
|
63
|
+
(await script.next_call()).applied()
|
|
64
64
|
script.thread["m1"] = {
|
|
65
65
|
"parentId": None, "role": "user", "isLeaf": True, "onActiveBranch": True
|
|
66
66
|
}
|
|
@@ -82,7 +82,7 @@ async def test_active_branch_anchor_accepted_off_branch_and_unknown_rejected():
|
|
|
82
82
|
)
|
|
83
83
|
async with run_host(script) as (drv, host):
|
|
84
84
|
drv.post("run/enqueue", add("m1", anchor="a1"))
|
|
85
|
-
(await script.next_call()).
|
|
85
|
+
(await script.next_call()).applied()
|
|
86
86
|
assert_rejected(
|
|
87
87
|
await drv.command("run/enqueue", add("m2", anchor="old"), terminal=False),
|
|
88
88
|
"wrong-anchor",
|
|
@@ -97,7 +97,7 @@ async def test_queued_item_and_dispatched_ids_are_valid_anchors():
|
|
|
97
97
|
script = Script()
|
|
98
98
|
async with run_host(script) as (drv, host):
|
|
99
99
|
drv.post("run/enqueue", add("m1"))
|
|
100
|
-
(await script.next_call()).
|
|
100
|
+
(await script.next_call()).applied()
|
|
101
101
|
await drv.command("run/enqueue", add("m2", anchor="m1"), terminal=False)
|
|
102
102
|
await drv.command("run/steer", add("s1", anchor="m2"), terminal=False)
|
|
103
103
|
assert queue_ids(drv.replica, "queue") == ["m2"]
|
|
@@ -113,7 +113,7 @@ async def test_replaced_tail_anchor_rejects_after_an_edit_rewrite():
|
|
|
113
113
|
)
|
|
114
114
|
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
115
115
|
drv.post("run/edit", edit_params("u1", msg("m2"), anchor=None))
|
|
116
|
-
(await script.next_call()).
|
|
116
|
+
(await script.next_call()).applied()
|
|
117
117
|
script.thread["u1"] = {
|
|
118
118
|
"parentId": None, "role": "user", "isLeaf": True, "onActiveBranch": False
|
|
119
119
|
}
|
|
@@ -128,7 +128,7 @@ async def test_replaced_tail_anchor_rejects_after_an_edit_rewrite():
|
|
|
128
128
|
assert res["type"] == "pending"
|
|
129
129
|
|
|
130
130
|
|
|
131
|
-
async def
|
|
131
|
+
async def test_unapplied_edit_crash_invalidates_the_replacement_anchor():
|
|
132
132
|
script = Script()
|
|
133
133
|
script.thread["u1"] = {
|
|
134
134
|
"parentId": None, "role": "user", "isLeaf": True, "onActiveBranch": True
|
|
@@ -146,12 +146,12 @@ async def test_unacked_edit_crash_invalidates_the_replacement_anchor():
|
|
|
146
146
|
assert [m["id"] for m in call.ctx.messages] == ["q1"]
|
|
147
147
|
|
|
148
148
|
|
|
149
|
-
async def
|
|
149
|
+
async def test_unapplied_recovery_run_keeps_applied_dispatch_anchors():
|
|
150
150
|
script = Script()
|
|
151
151
|
async with run_host(script, capabilities=("incomplete-continuation",)) as (drv, host):
|
|
152
152
|
drv.post("run/enqueue", add("m1"))
|
|
153
153
|
call = await script.next_call()
|
|
154
|
-
call.
|
|
154
|
+
call.applied()
|
|
155
155
|
call.fail(RuntimeError("boom"))
|
|
156
156
|
await drv.wait_status("error")
|
|
157
157
|
await drv.command("run/continue", terminal=False)
|
|
@@ -167,7 +167,7 @@ async def test_anchor_on_the_queued_edit_form_is_validated_never_ignored():
|
|
|
167
167
|
script = Script()
|
|
168
168
|
async with run_host(script) as (drv, host):
|
|
169
169
|
drv.post("run/enqueue", add("m1"))
|
|
170
|
-
(await script.next_call()).
|
|
170
|
+
(await script.next_call()).applied()
|
|
171
171
|
await drv.command("run/enqueue", add("m2", anchor="m1"), terminal=False)
|
|
172
172
|
assert_rejected(
|
|
173
173
|
await drv.command(
|
|
@@ -197,7 +197,7 @@ async def test_post_reload_sends_anchor_on_the_reload_targets_parent():
|
|
|
197
197
|
drv.post("run/reload", reload_params("a1", anchor="u1"))
|
|
198
198
|
call = await script.next_call()
|
|
199
199
|
assert call.ctx.trigger == "message-reload"
|
|
200
|
-
call.
|
|
200
|
+
call.applied()
|
|
201
201
|
script.thread["a1"] = {
|
|
202
202
|
"parentId": "u1", "role": "assistant", "isLeaf": True, "onActiveBranch": False
|
|
203
203
|
}
|