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.
Files changed (24) hide show
  1. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/PKG-INFO +1 -1
  2. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/pyproject.toml +1 -1
  3. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/src/harness_sdk/run_manager.py +23 -38
  4. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/run_helpers.py +11 -0
  5. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_branch_anchor.py +8 -16
  6. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_caller.py +2 -1
  7. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_edit_reload.py +9 -9
  8. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_facade.py +1 -0
  9. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_input_required.py +1 -1
  10. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_outcomes.py +2 -0
  11. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_rewind_during_run.py +5 -4
  12. harness_sdk_python-0.4.1/tests/test_run_leaf.py +116 -0
  13. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/.gitignore +0 -0
  14. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/README.md +0 -0
  15. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/src/harness_sdk/__init__.py +0 -0
  16. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/src/harness_sdk/fenced_postgres.py +0 -0
  17. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_batches.py +0 -0
  18. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_edit_dispatched.py +0 -0
  19. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_enqueue.py +0 -0
  20. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_fenced_postgres.py +0 -0
  21. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_placement.py +0 -0
  22. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_settle.py +0 -0
  23. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_steer.py +0 -0
  24. {harness_sdk_python-0.4.0 → harness_sdk_python-0.4.1}/tests/test_stop_continue.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: harness-sdk-python
3
- Version: 0.4.0
3
+ Version: 0.4.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "harness-sdk-python"
3
- version = "0.4.0"
3
+ version = "0.4.1"
4
4
  description = "RunManager: the harness-sdk runs subsystem for Python Statewire hosts"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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
- self._reload_message_id = message_id
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 or e.anchor == self._reload_message_id:
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 or e.message_id == self._reload_message_id:
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, message_id, ack))
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 test_reload_requires_a_message_id_and_delivers_it_on_the_start_context():
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
- assert_rejected(
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
- assert call.ctx.message_id == "r9"
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("ready")
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", "messageId": "r1"}, terminal=False))[
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", "messageId": "r1"}, terminal=False)
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", "messageId": "r1"}, terminal=False), "invalid-message"
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", "messageId": "r1"}, terminal=False), "unknown-id"
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", "messageId": "r1"}, terminal=False), "capability-missing"
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", "messageId": "r1"}, terminal=False), "capability-missing"
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", "messageId": "r1"}, terminal=False), "not-leaf"
181
+ await drv.command("run/reload", {"sourceId": "a0"}, terminal=False), "not-leaf"
182
182
  )
183
- res = await drv.command("run/reload", {"sourceId": "a1", "messageId": "r1"}, terminal=False)
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", "messageId": "r1"}, terminal=False), "capability-missing"
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", "messageId": "r1"}, terminal=False)
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"
@@ -24,6 +24,7 @@ def make_facade_host(script: Script):
24
24
  get_message_meta=script.get_message_meta,
25
25
  create_task=self.create_task,
26
26
  schedule=self.schedule,
27
+ leaf_message_id=script.leaf_id(),
27
28
  )
28
29
  yield
29
30
 
@@ -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", "messageId": "r1"}, terminal=False), "wrong-state"
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", "messageId": "r1"}, terminal=False
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", "messageId": "r1"}, terminal=False), "capability-missing"
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", "messageId": "r1"}, terminal=False), "not-leaf"
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", "messageId": "r1"}, terminal=False)
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
+ )