harness-sdk-python 0.4.0__tar.gz → 0.4.1__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.4.0 → harness_sdk_python-0.4.1}/PKG-INFO +1 -1
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/pyproject.toml +1 -1
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/src/harness_sdk/run_manager.py +23 -38
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/run_helpers.py +11 -0
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_branch_anchor.py +8 -16
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_caller.py +2 -1
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_edit_reload.py +9 -9
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_facade.py +1 -0
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_input_required.py +1 -1
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_outcomes.py +2 -0
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_rewind_during_run.py +5 -4
- harness_sdk_python-0.4.1/tests/test_run_leaf.py +116 -0
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/.gitignore +0 -0
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/README.md +0 -0
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_batches.py +0 -0
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_edit_dispatched.py +0 -0
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_enqueue.py +0 -0
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_placement.py +0 -0
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_settle.py +0 -0
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_steer.py +0 -0
- {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_stop_continue.py +0 -0
|
@@ -90,7 +90,6 @@ class _Edit:
|
|
|
90
90
|
@dataclass
|
|
91
91
|
class _Reload:
|
|
92
92
|
source_meta: dict[str, Any]
|
|
93
|
-
message_id: str
|
|
94
93
|
ack: Callable[[], None]
|
|
95
94
|
future: "asyncio.Future[Any]" = field(default_factory=_future)
|
|
96
95
|
|
|
@@ -119,7 +118,6 @@ class _Rewind:
|
|
|
119
118
|
rollback_to: Any
|
|
120
119
|
ack: Callable[[], None]
|
|
121
120
|
future: "asyncio.Future[Any]"
|
|
122
|
-
message_id: str | None = None
|
|
123
121
|
acked: bool = False
|
|
124
122
|
|
|
125
123
|
|
|
@@ -139,6 +137,7 @@ class RunManager:
|
|
|
139
137
|
get_message_meta: GetMessageMeta,
|
|
140
138
|
create_task: Callable[[Any], "asyncio.Task[Any]"],
|
|
141
139
|
schedule: Callable[[Callable[[], None]], None],
|
|
140
|
+
leaf_message_id: str | None,
|
|
142
141
|
capabilities: Iterable[str] = (),
|
|
143
142
|
max_queued: int = 50,
|
|
144
143
|
) -> None:
|
|
@@ -150,6 +149,10 @@ class RunManager:
|
|
|
150
149
|
raise ValueError("rewind-during-run requires the rewind capability")
|
|
151
150
|
if max_queued < 1:
|
|
152
151
|
raise ValueError("max_queued must be >= 1")
|
|
152
|
+
if leaf_message_id is not None and (
|
|
153
|
+
not isinstance(leaf_message_id, str) or leaf_message_id == ""
|
|
154
|
+
):
|
|
155
|
+
raise ValueError("leaf_message_id must be a non-empty string or None")
|
|
153
156
|
self._state = state
|
|
154
157
|
self._start = start
|
|
155
158
|
self._get_message_meta = get_message_meta
|
|
@@ -160,7 +163,6 @@ class RunManager:
|
|
|
160
163
|
self._task: "asyncio.Task[Any] | None" = None
|
|
161
164
|
self._ctx: "RunManager.StartContext | None" = None
|
|
162
165
|
self._dispatched_ids: tuple[str, ...] = ()
|
|
163
|
-
self._reload_message_id: str | None = None
|
|
164
166
|
self._dispatch_record: dict[str, Any] | None = None
|
|
165
167
|
self._callers: dict[str, StatewireClientHandle] = {}
|
|
166
168
|
self._intake: list[Any] = []
|
|
@@ -172,14 +174,15 @@ class RunManager:
|
|
|
172
174
|
self._run_futures: list["asyncio.Future[Any]"] = []
|
|
173
175
|
self._input_requests: list[dict[str, Any]] = []
|
|
174
176
|
self._input_answers: dict[str, Any] = {}
|
|
175
|
-
self._init_state()
|
|
177
|
+
self._init_state(leaf_message_id)
|
|
176
178
|
|
|
177
|
-
def _init_state(self) -> None:
|
|
179
|
+
def _init_state(self, leaf_message_id: str | None) -> None:
|
|
178
180
|
# Runs state is write-only and not durable: overwrite whatever is there.
|
|
179
181
|
self._state["status"] = "ready"
|
|
180
182
|
self._state["error"] = None
|
|
181
183
|
self._state["queue"] = []
|
|
182
184
|
self._state["steerQueue"] = []
|
|
185
|
+
self._state["runLeafMessageId"] = leaf_message_id
|
|
183
186
|
self._state.pop("dispatch", None)
|
|
184
187
|
self._state.pop("inputRequests", None)
|
|
185
188
|
|
|
@@ -221,14 +224,12 @@ class RunManager:
|
|
|
221
224
|
self._staged_stops.clear()
|
|
222
225
|
if isinstance(outcome, RunManager.Complete):
|
|
223
226
|
self._dispatched_ids = ()
|
|
224
|
-
self._reload_message_id = None
|
|
225
227
|
if self._staged_rewinds:
|
|
226
228
|
rewind = self._staged_rewinds.pop(0)
|
|
227
229
|
self._dispatch(
|
|
228
230
|
rewind.type,
|
|
229
231
|
rewind.messages,
|
|
230
232
|
rollback_to=rewind.rollback_to,
|
|
231
|
-
message_id=rewind.message_id,
|
|
232
233
|
)
|
|
233
234
|
rewind.ack()
|
|
234
235
|
if not rewind.future.done():
|
|
@@ -321,7 +322,6 @@ class RunManager:
|
|
|
321
322
|
*,
|
|
322
323
|
rollback_to: Any = _ABSENT,
|
|
323
324
|
responses: Any = _ABSENT,
|
|
324
|
-
message_id: str | None = None,
|
|
325
325
|
) -> None:
|
|
326
326
|
if type not in _ENTRY_TYPES:
|
|
327
327
|
raise ValueError(f"invalid entry type: {type!r}")
|
|
@@ -339,14 +339,15 @@ class RunManager:
|
|
|
339
339
|
record["rollbackTo"] = rollback_to
|
|
340
340
|
if responses is not _ABSENT:
|
|
341
341
|
record["responses"] = responses
|
|
342
|
-
if message_id is not None:
|
|
343
|
-
record["messageId"] = message_id
|
|
344
342
|
self._dispatch_record = record
|
|
345
343
|
self._stop_reason = None
|
|
346
344
|
self._state["error"] = None
|
|
347
345
|
if messages:
|
|
348
346
|
self._dispatched_ids = tuple(m["id"] for m in messages)
|
|
349
|
-
|
|
347
|
+
if rollback_to is not _ABSENT:
|
|
348
|
+
self._state["runLeafMessageId"] = rollback_to
|
|
349
|
+
if messages:
|
|
350
|
+
self._state["runLeafMessageId"] = messages[-1]["id"]
|
|
350
351
|
self._state["status"] = "running"
|
|
351
352
|
ctx = RunManager.StartContext(
|
|
352
353
|
type=type,
|
|
@@ -356,7 +357,6 @@ class RunManager:
|
|
|
356
357
|
_manager=self,
|
|
357
358
|
_rollback_to=rollback_to,
|
|
358
359
|
_responses=responses,
|
|
359
|
-
_message_id=message_id,
|
|
360
360
|
)
|
|
361
361
|
self._ctx = ctx
|
|
362
362
|
self._task = self._create_task(self._run(ctx))
|
|
@@ -529,7 +529,7 @@ class RunManager:
|
|
|
529
529
|
return
|
|
530
530
|
if self._lane_of(e.anchor) is not None:
|
|
531
531
|
return
|
|
532
|
-
if e.anchor in self._dispatched_ids
|
|
532
|
+
if e.anchor in self._dispatched_ids:
|
|
533
533
|
return
|
|
534
534
|
if e.anchor_meta is None:
|
|
535
535
|
raise _reject("unknown-id", f"anchor {e.anchor} names nothing")
|
|
@@ -621,7 +621,7 @@ class RunManager:
|
|
|
621
621
|
return None
|
|
622
622
|
if e.message_id in self._dispatched_ids:
|
|
623
623
|
return self._park_dispatched_edit(e)
|
|
624
|
-
if e.meta is not None
|
|
624
|
+
if e.meta is not None:
|
|
625
625
|
raise _reject(
|
|
626
626
|
"duplicate-id", f"message id {e.message_id} is already used"
|
|
627
627
|
)
|
|
@@ -735,14 +735,6 @@ class RunManager:
|
|
|
735
735
|
|
|
736
736
|
def _apply_reload(self, e: _Reload) -> Any:
|
|
737
737
|
self._check_leaf_lanes(e.source_meta, "run/reload")
|
|
738
|
-
if (
|
|
739
|
-
self._lane_of(e.message_id) is not None
|
|
740
|
-
or e.message_id in self._dispatched_ids
|
|
741
|
-
or e.message_id == self._reload_message_id
|
|
742
|
-
):
|
|
743
|
-
raise _reject(
|
|
744
|
-
"duplicate-id", f"message id {e.message_id} is already used"
|
|
745
|
-
)
|
|
746
738
|
self._staged_rewinds.append(
|
|
747
739
|
_Rewind(
|
|
748
740
|
"message-reload",
|
|
@@ -750,7 +742,6 @@ class RunManager:
|
|
|
750
742
|
e.source_meta["parentId"],
|
|
751
743
|
e.ack,
|
|
752
744
|
e.future,
|
|
753
|
-
message_id=e.message_id,
|
|
754
745
|
)
|
|
755
746
|
)
|
|
756
747
|
return _PARKED
|
|
@@ -896,11 +887,6 @@ class RunManager:
|
|
|
896
887
|
source_id = params.get("sourceId") if isinstance(params, dict) else None
|
|
897
888
|
if not isinstance(source_id, str):
|
|
898
889
|
raise _reject("invalid-message", "sourceId must be a string")
|
|
899
|
-
message_id = params.get("messageId")
|
|
900
|
-
if not isinstance(message_id, str) or message_id == "":
|
|
901
|
-
raise _reject("invalid-message", "messageId must be a non-empty string")
|
|
902
|
-
if await self._get_message_meta(message_id) is not None:
|
|
903
|
-
raise _reject("duplicate-id", f"message id {message_id} is already used")
|
|
904
890
|
meta = await self._get_message_meta(source_id)
|
|
905
891
|
if meta is None:
|
|
906
892
|
raise _reject("unknown-id", f"message {source_id} is unknown")
|
|
@@ -918,7 +904,7 @@ class RunManager:
|
|
|
918
904
|
"capability-missing",
|
|
919
905
|
"the assistant-continuation capability is not enabled",
|
|
920
906
|
)
|
|
921
|
-
return await self._stage(_Reload(meta,
|
|
907
|
+
return await self._stage(_Reload(meta, ack))
|
|
922
908
|
|
|
923
909
|
async def stop(self, params: Any = None, *, ack: Callable[[], None]) -> Any:
|
|
924
910
|
if params is not None and not isinstance(params, dict):
|
|
@@ -1052,20 +1038,11 @@ class RunManager:
|
|
|
1052
1038
|
_manager: "RunManager"
|
|
1053
1039
|
_rollback_to: Any
|
|
1054
1040
|
_responses: Any
|
|
1055
|
-
_message_id: str | None
|
|
1056
1041
|
|
|
1057
1042
|
@property
|
|
1058
1043
|
def stop_reason(self) -> str | None:
|
|
1059
1044
|
return self._manager._stop_reason
|
|
1060
1045
|
|
|
1061
|
-
@property
|
|
1062
|
-
def message_id(self) -> str:
|
|
1063
|
-
if self._message_id is None:
|
|
1064
|
-
raise AttributeError(
|
|
1065
|
-
"message_id is only present on message-reload entries"
|
|
1066
|
-
)
|
|
1067
|
-
return self._message_id
|
|
1068
|
-
|
|
1069
1046
|
@property
|
|
1070
1047
|
def has_rollback(self) -> bool:
|
|
1071
1048
|
return self._rollback_to is not _ABSENT
|
|
@@ -1101,10 +1078,18 @@ class RunManager:
|
|
|
1101
1078
|
for item in items:
|
|
1102
1079
|
self._manager._callers.pop(item["id"], None)
|
|
1103
1080
|
self._manager._adopt_entities(item["id"] for item in items)
|
|
1081
|
+
if items:
|
|
1082
|
+
self._manager._state["runLeafMessageId"] = items[-1]["id"]
|
|
1104
1083
|
return tuple(
|
|
1105
1084
|
{k: v for k, v in item.items() if k != "caller"} for item in items
|
|
1106
1085
|
)
|
|
1107
1086
|
|
|
1087
|
+
def set_leaf_message_id(self, message_id: str) -> None:
|
|
1088
|
+
self._ensure_active()
|
|
1089
|
+
if not isinstance(message_id, str) or message_id == "":
|
|
1090
|
+
raise ValueError("message_id must be a non-empty string")
|
|
1091
|
+
self._manager._state["runLeafMessageId"] = message_id
|
|
1092
|
+
|
|
1108
1093
|
def set_recovery_state(self, value: Any) -> None:
|
|
1109
1094
|
self._ensure_active()
|
|
1110
1095
|
record = self._manager._dispatch_record
|
|
@@ -40,6 +40,16 @@ class Script:
|
|
|
40
40
|
return {"isLeaf": not self.thread}
|
|
41
41
|
return self.thread.get(message_id)
|
|
42
42
|
|
|
43
|
+
def leaf_id(self) -> str | None:
|
|
44
|
+
return next(
|
|
45
|
+
(
|
|
46
|
+
id
|
|
47
|
+
for id, meta in self.thread.items()
|
|
48
|
+
if meta.get("isLeaf") and meta.get("onActiveBranch", True)
|
|
49
|
+
),
|
|
50
|
+
None,
|
|
51
|
+
)
|
|
52
|
+
|
|
43
53
|
async def next_call(self, timeout: float = 5) -> Call:
|
|
44
54
|
return await asyncio.wait_for(self.calls.get(), timeout)
|
|
45
55
|
|
|
@@ -62,6 +72,7 @@ def make_host(script: Script, capabilities=(), initial_runs=None, max_queued=50)
|
|
|
62
72
|
get_message_meta=script.get_message_meta,
|
|
63
73
|
create_task=self.create_task,
|
|
64
74
|
schedule=self.schedule,
|
|
75
|
+
leaf_message_id=script.leaf_id(),
|
|
65
76
|
capabilities=capabilities,
|
|
66
77
|
max_queued=max_queued,
|
|
67
78
|
)
|
|
@@ -115,7 +115,7 @@ async def test_anchor_on_the_queued_edit_form_is_validated_never_ignored():
|
|
|
115
115
|
assert drv.replica["queue"][0]["parts"][0]["text"] == "edited"
|
|
116
116
|
|
|
117
117
|
|
|
118
|
-
async def
|
|
118
|
+
async def test_post_reload_sends_anchor_on_the_reload_targets_parent():
|
|
119
119
|
script = Script()
|
|
120
120
|
script.thread.update(
|
|
121
121
|
{
|
|
@@ -124,22 +124,14 @@ async def test_reload_requires_a_message_id_and_delivers_it_on_the_start_context
|
|
|
124
124
|
}
|
|
125
125
|
)
|
|
126
126
|
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
127
|
-
|
|
128
|
-
await drv.command("run/reload", {"sourceId": "a1"}, terminal=False),
|
|
129
|
-
"invalid-message",
|
|
130
|
-
)
|
|
131
|
-
assert_rejected(
|
|
132
|
-
await drv.command(
|
|
133
|
-
"run/reload", {"sourceId": "a1", "messageId": "u1"}, terminal=False
|
|
134
|
-
),
|
|
135
|
-
"duplicate-id",
|
|
136
|
-
)
|
|
137
|
-
res = await drv.command(
|
|
138
|
-
"run/reload", {"sourceId": "a1", "messageId": "r9"}, terminal=False
|
|
139
|
-
)
|
|
127
|
+
res = await drv.command("run/reload", {"sourceId": "a1"}, terminal=False)
|
|
140
128
|
assert res["type"] == "pending"
|
|
141
129
|
call = await script.next_call()
|
|
142
130
|
assert call.ctx.type == "message-reload"
|
|
143
|
-
|
|
131
|
+
script.thread["a1"] = {
|
|
132
|
+
"parentId": "u1", "role": "assistant", "isLeaf": True, "onActiveBranch": False
|
|
133
|
+
}
|
|
134
|
+
res = await drv.command("run/enqueue", add("m1", anchor="u1"), terminal=False)
|
|
135
|
+
assert res["type"] == "pending"
|
|
144
136
|
call.finish(RunManager.Complete())
|
|
145
|
-
await drv.wait_status("
|
|
137
|
+
await drv.wait_status("running")
|
|
@@ -47,7 +47,7 @@ async def test_dispatch_without_queue_entry_has_no_caller():
|
|
|
47
47
|
script = Script()
|
|
48
48
|
async with run_host(script, capabilities=("rewind",)) as (drv, _):
|
|
49
49
|
script.thread["a1"] = {"role": "assistant", "parentId": None, "isLeaf": True}
|
|
50
|
-
assert (await drv.command("run/reload", {"sourceId": "a1"
|
|
50
|
+
assert (await drv.command("run/reload", {"sourceId": "a1"}, terminal=False))[
|
|
51
51
|
"type"
|
|
52
52
|
] == "pending"
|
|
53
53
|
call = await script.next_call()
|
|
@@ -109,5 +109,6 @@ async def test_max_queued_below_one_rejects_at_construction():
|
|
|
109
109
|
get_message_meta=script.get_message_meta,
|
|
110
110
|
create_task=lambda coro: None,
|
|
111
111
|
schedule=lambda fn: None,
|
|
112
|
+
leaf_message_id=None,
|
|
112
113
|
max_queued=0,
|
|
113
114
|
)
|
|
@@ -125,7 +125,7 @@ async def test_reload_dispatches_with_empty_messages():
|
|
|
125
125
|
script = Script()
|
|
126
126
|
thread_with_turn(script)
|
|
127
127
|
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
128
|
-
res = await drv.command("run/reload", {"sourceId": "a1"
|
|
128
|
+
res = await drv.command("run/reload", {"sourceId": "a1"}, terminal=False)
|
|
129
129
|
assert res["type"] == "pending"
|
|
130
130
|
call = await script.next_call()
|
|
131
131
|
assert call.ctx.type == "message-reload"
|
|
@@ -140,15 +140,15 @@ async def test_reload_rejections():
|
|
|
140
140
|
thread_with_turn(script)
|
|
141
141
|
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
142
142
|
assert_rejected(
|
|
143
|
-
await drv.command("run/reload", {"sourceId": "u1"
|
|
143
|
+
await drv.command("run/reload", {"sourceId": "u1"}, terminal=False), "invalid-message"
|
|
144
144
|
)
|
|
145
145
|
assert_rejected(
|
|
146
|
-
await drv.command("run/reload", {"sourceId": "zz"
|
|
146
|
+
await drv.command("run/reload", {"sourceId": "zz"}, terminal=False), "unknown-id"
|
|
147
147
|
)
|
|
148
148
|
await drv.command("run/enqueue", add("m1", anchor="a1"), terminal=False)
|
|
149
149
|
await script.next_call()
|
|
150
150
|
assert_rejected(
|
|
151
|
-
await drv.command("run/reload", {"sourceId": "a1"
|
|
151
|
+
await drv.command("run/reload", {"sourceId": "a1"}, terminal=False), "capability-missing"
|
|
152
152
|
)
|
|
153
153
|
|
|
154
154
|
|
|
@@ -157,7 +157,7 @@ async def test_reload_without_rewind_rejects():
|
|
|
157
157
|
thread_with_turn(script)
|
|
158
158
|
async with run_host(script) as (drv, host):
|
|
159
159
|
assert_rejected(
|
|
160
|
-
await drv.command("run/reload", {"sourceId": "a1"
|
|
160
|
+
await drv.command("run/reload", {"sourceId": "a1"}, terminal=False), "capability-missing"
|
|
161
161
|
)
|
|
162
162
|
|
|
163
163
|
|
|
@@ -178,9 +178,9 @@ async def test_reload_leaf_only_while_queue_non_empty():
|
|
|
178
178
|
call.fail(RuntimeError("boom"))
|
|
179
179
|
await drv.wait_status("error")
|
|
180
180
|
assert_rejected(
|
|
181
|
-
await drv.command("run/reload", {"sourceId": "a0"
|
|
181
|
+
await drv.command("run/reload", {"sourceId": "a0"}, terminal=False), "not-leaf"
|
|
182
182
|
)
|
|
183
|
-
res = await drv.command("run/reload", {"sourceId": "a1"
|
|
183
|
+
res = await drv.command("run/reload", {"sourceId": "a1"}, terminal=False)
|
|
184
184
|
assert res["type"] == "pending"
|
|
185
185
|
call = await script.next_call()
|
|
186
186
|
assert call.ctx.rollback_to == "m1"
|
|
@@ -196,12 +196,12 @@ async def test_reload_of_assistant_parented_response_needs_continuation():
|
|
|
196
196
|
)
|
|
197
197
|
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
198
198
|
assert_rejected(
|
|
199
|
-
await drv.command("run/reload", {"sourceId": "a2"
|
|
199
|
+
await drv.command("run/reload", {"sourceId": "a2"}, terminal=False), "capability-missing"
|
|
200
200
|
)
|
|
201
201
|
async with run_host(script, capabilities=("rewind", "assistant-continuation")) as (
|
|
202
202
|
drv,
|
|
203
203
|
host,
|
|
204
204
|
):
|
|
205
|
-
res = await drv.command("run/reload", {"sourceId": "a2"
|
|
205
|
+
res = await drv.command("run/reload", {"sourceId": "a2"}, terminal=False)
|
|
206
206
|
assert res["type"] == "pending"
|
|
207
207
|
assert (await script.next_call()).ctx.rollback_to == "a1"
|
|
@@ -264,7 +264,7 @@ async def test_lifecycle_commands_rejected_while_parked():
|
|
|
264
264
|
"wrong-state",
|
|
265
265
|
)
|
|
266
266
|
assert_rejected(
|
|
267
|
-
await drv.command("run/reload", {"sourceId": "a1"
|
|
267
|
+
await drv.command("run/reload", {"sourceId": "a1"}, terminal=False), "wrong-state"
|
|
268
268
|
)
|
|
269
269
|
|
|
270
270
|
|
|
@@ -21,6 +21,7 @@ async def test_register_overwrites_whatever_is_in_the_state():
|
|
|
21
21
|
"error": None,
|
|
22
22
|
"queue": [],
|
|
23
23
|
"steerQueue": [],
|
|
24
|
+
"runLeafMessageId": None,
|
|
24
25
|
}
|
|
25
26
|
script.no_call()
|
|
26
27
|
|
|
@@ -126,5 +127,6 @@ async def test_unknown_capability_rejects_at_construction():
|
|
|
126
127
|
get_message_meta=None,
|
|
127
128
|
create_task=asyncio.create_task,
|
|
128
129
|
schedule=lambda fn: None,
|
|
130
|
+
leaf_message_id=None,
|
|
129
131
|
capabilities=("time-travel",),
|
|
130
132
|
)
|
|
@@ -58,7 +58,7 @@ async def test_running_leaf_reload_stops_settles_then_rewinds():
|
|
|
58
58
|
call = await start_run(drv, script)
|
|
59
59
|
await drv.command("run/enqueue", add("q1", anchor="a1"), terminal=False)
|
|
60
60
|
pending = await drv.command(
|
|
61
|
-
"run/reload", {"sourceId": "a1"
|
|
61
|
+
"run/reload", {"sourceId": "a1"}, terminal=False
|
|
62
62
|
)
|
|
63
63
|
assert pending["type"] == "pending"
|
|
64
64
|
await asyncio.wait_for(call.ctx.stop_requested.wait(), 5)
|
|
@@ -81,7 +81,7 @@ async def test_running_edit_and_reload_reject_without_capability(capabilities):
|
|
|
81
81
|
"capability-missing",
|
|
82
82
|
)
|
|
83
83
|
assert_rejected(
|
|
84
|
-
await drv.command("run/reload", {"sourceId": "a1"
|
|
84
|
+
await drv.command("run/reload", {"sourceId": "a1"}, terminal=False), "capability-missing"
|
|
85
85
|
)
|
|
86
86
|
|
|
87
87
|
|
|
@@ -94,7 +94,7 @@ async def test_running_deeper_than_leaf_edit_and_reload_reject():
|
|
|
94
94
|
"not-leaf",
|
|
95
95
|
)
|
|
96
96
|
assert_rejected(
|
|
97
|
-
await drv.command("run/reload", {"sourceId": "a1"
|
|
97
|
+
await drv.command("run/reload", {"sourceId": "a1"}, terminal=False), "not-leaf"
|
|
98
98
|
)
|
|
99
99
|
|
|
100
100
|
|
|
@@ -107,6 +107,7 @@ async def test_rewind_during_run_requires_rewind():
|
|
|
107
107
|
get_message_meta=script.get_message_meta,
|
|
108
108
|
create_task=asyncio.create_task,
|
|
109
109
|
schedule=lambda fn: None,
|
|
110
|
+
leaf_message_id=None,
|
|
110
111
|
capabilities=("rewind-during-run",),
|
|
111
112
|
)
|
|
112
113
|
|
|
@@ -118,7 +119,7 @@ async def test_leaf_reload_in_error_accepted_with_non_empty_queue():
|
|
|
118
119
|
await drv.command("run/enqueue", add("q1", anchor="a1"), terminal=False)
|
|
119
120
|
call.fail(RuntimeError("boom"))
|
|
120
121
|
await drv.wait_status("error")
|
|
121
|
-
res = await drv.command("run/reload", {"sourceId": "a1"
|
|
122
|
+
res = await drv.command("run/reload", {"sourceId": "a1"}, terminal=False)
|
|
122
123
|
assert res["type"] == "pending"
|
|
123
124
|
rerun = await script.next_call()
|
|
124
125
|
assert rerun.ctx.type == "message-reload"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from run_helpers import Script, add, msg, run_host
|
|
3
|
+
|
|
4
|
+
from harness_sdk import RunManager
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def thread_with_turn(script: Script) -> None:
|
|
8
|
+
script.thread.update(
|
|
9
|
+
{
|
|
10
|
+
"u1": {"parentId": None, "role": "user", "isLeaf": False, "onActiveBranch": True},
|
|
11
|
+
"a1": {"parentId": "u1", "role": "assistant", "isLeaf": True, "onActiveBranch": True},
|
|
12
|
+
}
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def test_seeds_null_on_an_empty_thread():
|
|
17
|
+
script = Script()
|
|
18
|
+
async with run_host(script) as (drv, host):
|
|
19
|
+
assert drv.replica["runLeafMessageId"] is None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def test_seeds_the_hosts_leaf_on_a_pre_existing_thread():
|
|
23
|
+
script = Script()
|
|
24
|
+
thread_with_turn(script)
|
|
25
|
+
async with run_host(script) as (drv, host):
|
|
26
|
+
assert drv.replica["runLeafMessageId"] == "a1"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def test_enqueue_dispatch_sets_the_leaf():
|
|
30
|
+
script = Script()
|
|
31
|
+
async with run_host(script) as (drv, host):
|
|
32
|
+
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
33
|
+
await script.next_call()
|
|
34
|
+
await drv.wait(lambda s: s["runLeafMessageId"] == "m1")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def test_steer_dispatch_sets_the_leaf_to_the_last_lane_item():
|
|
38
|
+
script = Script()
|
|
39
|
+
async with run_host(script) as (drv, host):
|
|
40
|
+
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
41
|
+
call = await script.next_call()
|
|
42
|
+
await drv.command("run/steer", add("s1", anchor="m1"), terminal=False)
|
|
43
|
+
await drv.command("run/steer", add("s2", anchor="s1"), terminal=False)
|
|
44
|
+
call.finish(RunManager.Complete())
|
|
45
|
+
steered = await script.next_call()
|
|
46
|
+
assert steered.ctx.messages[-1]["id"] == "s2"
|
|
47
|
+
await drv.wait(lambda s: s["runLeafMessageId"] == "s2")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def test_take_steered_sets_the_leaf():
|
|
51
|
+
script = Script()
|
|
52
|
+
async with run_host(script) as (drv, host):
|
|
53
|
+
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
54
|
+
call = await script.next_call()
|
|
55
|
+
await drv.command("run/steer", add("s1", anchor="m1"), terminal=False)
|
|
56
|
+
await drv.wait(lambda s: len(s["steerQueue"]) == 1)
|
|
57
|
+
call.ctx.take_steered()
|
|
58
|
+
await drv.wait(lambda s: s["runLeafMessageId"] == "s1")
|
|
59
|
+
call.finish(RunManager.Complete())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def test_executor_report_sets_the_leaf():
|
|
63
|
+
script = Script()
|
|
64
|
+
async with run_host(script) as (drv, host):
|
|
65
|
+
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
66
|
+
call = await script.next_call()
|
|
67
|
+
call.ctx.set_leaf_message_id("a1")
|
|
68
|
+
await drv.wait(lambda s: s["runLeafMessageId"] == "a1")
|
|
69
|
+
call.finish(RunManager.Complete())
|
|
70
|
+
await drv.wait_status("ready")
|
|
71
|
+
assert drv.replica["runLeafMessageId"] == "a1"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def test_edit_dispatch_sets_the_leaf_to_the_replacement():
|
|
75
|
+
script = Script()
|
|
76
|
+
thread_with_turn(script)
|
|
77
|
+
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
78
|
+
await drv.command(
|
|
79
|
+
"run/edit", {"sourceId": "u1", "message": msg("u2")}, terminal=False
|
|
80
|
+
)
|
|
81
|
+
await script.next_call()
|
|
82
|
+
await drv.wait(lambda s: s["runLeafMessageId"] == "u2")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def test_reload_dispatch_sets_the_leaf_to_the_rollback_target():
|
|
86
|
+
script = Script()
|
|
87
|
+
thread_with_turn(script)
|
|
88
|
+
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
89
|
+
await drv.command("run/reload", {"sourceId": "a1"}, terminal=False)
|
|
90
|
+
call = await script.next_call()
|
|
91
|
+
await drv.wait(lambda s: s["runLeafMessageId"] == "u1")
|
|
92
|
+
call.ctx.set_leaf_message_id("a2")
|
|
93
|
+
await drv.wait(lambda s: s["runLeafMessageId"] == "a2")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def test_report_rejects_an_empty_id():
|
|
97
|
+
script = Script()
|
|
98
|
+
async with run_host(script) as (drv, host):
|
|
99
|
+
await drv.command("run/enqueue", add("m1"), terminal=False)
|
|
100
|
+
call = await script.next_call()
|
|
101
|
+
with pytest.raises(ValueError, match="non-empty"):
|
|
102
|
+
call.ctx.set_leaf_message_id("")
|
|
103
|
+
call.finish(RunManager.Complete())
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def test_construction_rejects_an_invalid_leaf_id():
|
|
107
|
+
script = Script()
|
|
108
|
+
with pytest.raises(ValueError, match="leaf_message_id"):
|
|
109
|
+
RunManager(
|
|
110
|
+
state={},
|
|
111
|
+
start=script.start,
|
|
112
|
+
get_message_meta=script.get_message_meta,
|
|
113
|
+
create_task=lambda coro: None,
|
|
114
|
+
schedule=lambda fn: None,
|
|
115
|
+
leaf_message_id="",
|
|
116
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|