harness-sdk-python 0.3.0__tar.gz → 0.3.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 (22) hide show
  1. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/.gitignore +3 -0
  2. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/PKG-INFO +1 -1
  3. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/pyproject.toml +1 -1
  4. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/src/harness_sdk/run_manager.py +49 -5
  5. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/tests/test_batches.py +12 -9
  6. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/tests/test_caller.py +21 -21
  7. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/tests/test_edit_dispatched.py +11 -9
  8. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/tests/test_edit_reload.py +36 -36
  9. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/tests/test_enqueue.py +41 -41
  10. harness_sdk_python-0.3.1/tests/test_facade.py +84 -0
  11. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/tests/test_input_required.py +6 -6
  12. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/tests/test_outcomes.py +11 -11
  13. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/tests/test_placement.py +18 -18
  14. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/tests/test_rewind_during_run.py +14 -11
  15. harness_sdk_python-0.3.1/tests/test_settle.py +131 -0
  16. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/tests/test_steer.py +16 -16
  17. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/tests/test_stop_continue.py +15 -15
  18. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/README.md +0 -0
  19. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/src/harness_sdk/__init__.py +0 -0
  20. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/src/harness_sdk/fenced_postgres.py +0 -0
  21. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/tests/run_helpers.py +0 -0
  22. {harness_sdk_python-0.3.0 → harness_sdk_python-0.3.1}/tests/test_fenced_postgres.py +0 -0
@@ -9,6 +9,9 @@ dist-test
9
9
  .dev.vars
10
10
  *.tsbuildinfo
11
11
  .harness-runs
12
+ *.rdb
13
+ .source
14
+ next-env.d.ts
12
15
  __pycache__
13
16
  .venv
14
17
  .pytest_cache
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: harness-sdk-python
3
- Version: 0.3.0
3
+ Version: 0.3.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.3.0"
3
+ version = "0.3.1"
4
4
  description = "RunManager: the harness-sdk runs subsystem for Python Statewire hosts"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -161,6 +161,8 @@ class RunManager:
161
161
  self._staged_stops: list["asyncio.Future[Any]"] = []
162
162
  self._stop_reason: str | None = None
163
163
  self._staged_rewinds: list[_Rewind] = []
164
+ self._entity_futures: dict[str, "asyncio.Future[Any]"] = {}
165
+ self._run_futures: list["asyncio.Future[Any]"] = []
164
166
  self._input_requests: list[dict[str, Any]] = []
165
167
  self._input_answers: dict[str, Any] = {}
166
168
  self._init_state()
@@ -217,8 +219,9 @@ class RunManager:
217
219
  self._dispatch(
218
220
  rewind.type, rewind.messages, rollback_to=rewind.rollback_to
219
221
  )
222
+ rewind.ack()
220
223
  if not rewind.future.done():
221
- rewind.future.set_result(None)
224
+ self._run_futures.append(rewind.future)
222
225
  elif outcome is not None:
223
226
  self._settle_outcome(outcome)
224
227
  else:
@@ -318,6 +321,7 @@ class RunManager:
318
321
  ]
319
322
  for message in messages:
320
323
  self._callers.pop(message["id"], None)
324
+ self._adopt_entities(message["id"] for message in messages)
321
325
  record: dict[str, Any] = {"type": type, "messages": list(messages)}
322
326
  if rollback_to is not _ABSENT:
323
327
  record["rollbackTo"] = rollback_to
@@ -359,10 +363,21 @@ class RunManager:
359
363
  )
360
364
  except Exception as exc:
361
365
  self._settle(ctx)
362
- self._freeze(str(exc) or type(exc).__name__)
366
+ message = str(exc) or type(exc).__name__
367
+ if isinstance(exc, StatewireReject):
368
+ self._freeze(exc.message, exc.payload)
369
+ self._settle_entities(exc)
370
+ else:
371
+ self._freeze(message)
372
+ self._settle_entities(_reject("run-error", message))
363
373
  self._drain()
364
374
  return
365
375
  self._settle(ctx)
376
+ self._settle_entities(
377
+ _reject("run-error", "run ended in error")
378
+ if isinstance(outcome, RunManager.Error)
379
+ else None
380
+ )
366
381
  self._outcome = outcome
367
382
  self._drain()
368
383
 
@@ -372,6 +387,22 @@ class RunManager:
372
387
  self._task = None
373
388
  self._dispatch_record = None
374
389
 
390
+ def _settle_entities(self, error: StatewireReject | None) -> None:
391
+ futures, self._run_futures = self._run_futures, []
392
+ for future in futures:
393
+ if future.done():
394
+ continue
395
+ if error is None:
396
+ future.set_result(None)
397
+ else:
398
+ future.set_exception(error)
399
+
400
+ def _adopt_entities(self, ids: Iterable[str]) -> None:
401
+ for id in ids:
402
+ future = self._entity_futures.pop(id, None)
403
+ if future is not None:
404
+ self._run_futures.append(future)
405
+
375
406
  def _pop_dispatchable(self) -> bool:
376
407
  steer = self._lane_items("steerQueue")
377
408
  if steer:
@@ -385,9 +416,14 @@ class RunManager:
385
416
  return True
386
417
  return False
387
418
 
388
- def _freeze(self, message: str) -> None:
419
+ def _freeze(self, message: str, payload: Any = None) -> None:
389
420
  self._state["status"] = "error"
390
- self._state["error"] = {"message": message}
421
+ if isinstance(payload, dict):
422
+ self._state["error"] = {**payload, "message": message}
423
+ elif payload is not None:
424
+ self._state["error"] = {"message": message, "payload": payload}
425
+ else:
426
+ self._state["error"] = {"message": message}
391
427
 
392
428
  # ─── Message and placement validation ───────────────────
393
429
 
@@ -556,10 +592,12 @@ class RunManager:
556
592
  )
557
593
  with self._caller_stamp(e.message_id, e.caller):
558
594
  self._insert_new(e.lane, e.message, e.params)
595
+ self._entity_futures[e.message_id] = e.future
596
+ e.ack()
559
597
  fx.new_added = True
560
598
  if e.lane == "steerQueue":
561
599
  fx.steer_added = True
562
- return None
600
+ return _PARKED
563
601
 
564
602
  def _apply_move(self, e: _Send, fx: _Effects) -> Any:
565
603
  current = self._lane_of(e.message_id)
@@ -608,6 +646,11 @@ class RunManager:
608
646
  self._state[lane] = [
609
647
  item for item in self._lane_items(lane) if item["id"] != e.message_id
610
648
  ]
649
+ entity = self._entity_futures.pop(e.message_id, None)
650
+ if entity is not None and not entity.done():
651
+ entity.set_exception(
652
+ _reject("removed", f"message {e.message_id} was removed from the queue")
653
+ )
611
654
  return None
612
655
 
613
656
  def _apply_input(self, e: _Input) -> Any:
@@ -956,6 +999,7 @@ class RunManager:
956
999
  self._manager._state["steerQueue"] = []
957
1000
  for item in items:
958
1001
  self._manager._callers.pop(item["id"], None)
1002
+ self._manager._adopt_entities(item["id"] for item in items)
959
1003
  return tuple(
960
1004
  {k: v for k, v in item.items() if k != "caller"} for item in items
961
1005
  )
@@ -21,23 +21,26 @@ async def test_multi_steer_batch_places_all_and_dispatches_once():
21
21
  ]
22
22
  )
23
23
  for offset in range(3):
24
- assert (await drv.res(first + offset))["type"] == "accepted"
24
+ assert (await drv.res(first + offset, terminal=False))["type"] == "pending"
25
25
  call = await script.next_call()
26
26
  assert call.ctx.type == "message-send"
27
27
  assert [m["id"] for m in call.ctx.messages] == ["s1", "s2", "s3"]
28
28
  script.no_call()
29
29
  assert queue_ids(drv.replica, "steerQueue") == []
30
+ call.finish(RunManager.Complete())
31
+ for offset in range(3):
32
+ assert (await drv.res(first + offset))["type"] == "accepted"
30
33
 
31
34
 
32
35
  async def test_steer_and_stop_in_one_batch_nets_to_stop():
33
36
  script = Script()
34
37
  async with run_host(script) as (drv, host):
35
- await drv.command("run/enqueue", {"message": msg("m1")})
38
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
36
39
  call = await script.next_call()
37
40
  first = await drv.batch(
38
41
  [("run/steer", {"message": msg("s1")}), ("run/stop", None)]
39
42
  )
40
- assert (await drv.res(first))["type"] == "accepted"
43
+ assert (await drv.res(first, terminal=False))["type"] == "pending"
41
44
  assert (await drv.res(first + 1, terminal=False))["type"] == "pending"
42
45
  await asyncio.wait_for(call.ctx.stop_requested.wait(), 5)
43
46
  call.finish(RunManager.Stop(dispatch_queue=False))
@@ -50,7 +53,7 @@ async def test_steer_and_stop_in_one_batch_nets_to_stop():
50
53
  async def test_stop_settles_only_after_the_run_task_ends():
51
54
  script = Script()
52
55
  async with run_host(script) as (drv, host):
53
- await drv.command("run/enqueue", {"message": msg("m1")})
56
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
54
57
  call = await script.next_call()
55
58
  pending = await drv.command("run/stop", terminal=False)
56
59
  assert pending["type"] == "pending"
@@ -75,8 +78,8 @@ class TailScript(Script):
75
78
  async def test_handler_tail_staging_decides_in_its_own_envelope():
76
79
  script = TailScript()
77
80
  async with run_host(script) as (drv, host):
78
- res = await drv.command("run/enqueue", {"message": msg("m1")})
79
- assert res["type"] == "accepted"
81
+ res = await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
82
+ assert res["type"] == "pending"
80
83
  call = await script.next_call()
81
84
  assert [m["id"] for m in call.ctx.messages] == ["m1"]
82
85
  dispatch_env = next(
@@ -88,6 +91,6 @@ async def test_handler_tail_staging_decides_in_its_own_envelope():
88
91
  )
89
92
  )
90
93
  # The decide ran outside the batch segment: its ops flush in an
91
- # envelope of their own, before the command's covering ack.
92
- assert "res" not in dispatch_env
93
- assert dispatch_env.get("ack", 0) < res["seq"]
94
+ # envelope of their own, alongside the pending verdict and its ack.
95
+ assert dispatch_env["res"] == [{"seq": res["seq"], "type": "pending"}]
96
+ assert dispatch_env["ack"] >= res["seq"]
@@ -7,9 +7,9 @@ from harness_sdk import RunManager
7
7
  async def test_immediate_dispatch_carries_caller():
8
8
  script = Script()
9
9
  async with run_host(script) as (drv, _):
10
- assert (await drv.command("run/enqueue", {"message": msg("m1")}))[
10
+ assert (await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False))[
11
11
  "type"
12
- ] == "accepted"
12
+ ] == "pending"
13
13
  call = await script.next_call()
14
14
  assert call.ctx.caller is not None
15
15
  assert call.ctx.caller.client_id == "c1"
@@ -19,9 +19,9 @@ async def test_immediate_dispatch_carries_caller():
19
19
  async def test_queue_entry_projects_caller_client_id():
20
20
  script = Script()
21
21
  async with run_host(script) as (drv, _):
22
- await drv.command("run/enqueue", {"message": msg("m1")})
22
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
23
23
  call = await script.next_call()
24
- await drv.command("run/enqueue", {"message": msg("m2")})
24
+ await drv.command("run/enqueue", {"message": msg("m2")}, terminal=False)
25
25
  await drv.wait(lambda s: len(s["queue"]) == 1)
26
26
  assert drv.replica["queue"][0]["caller"] == {"clientId": "c1"}
27
27
  call.finish(RunManager.Complete())
@@ -30,9 +30,9 @@ async def test_queue_entry_projects_caller_client_id():
30
30
  async def test_caller_context_is_read_at_dispatch_time():
31
31
  script = Script()
32
32
  async with run_host(script) as (drv, _):
33
- await drv.command("run/enqueue", {"message": msg("m1")})
33
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
34
34
  call = await script.next_call()
35
- await drv.command("run/enqueue", {"message": msg("m2")})
35
+ await drv.command("run/enqueue", {"message": msg("m2")}, terminal=False)
36
36
  assert (await drv.context({"user": "simon"}))["type"] == "accepted"
37
37
  call.finish(RunManager.Complete())
38
38
  dispatched = await script.next_call()
@@ -47,9 +47,9 @@ 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
- ] == "accepted"
52
+ ] == "pending"
53
53
  call = await script.next_call()
54
54
  assert call.ctx.caller is None
55
55
  call.finish(RunManager.Complete())
@@ -58,9 +58,9 @@ async def test_dispatch_without_queue_entry_has_no_caller():
58
58
  async def test_take_steered_strips_the_caller_stamp():
59
59
  script = Script()
60
60
  async with run_host(script) as (drv, _):
61
- await drv.command("run/enqueue", {"message": msg("m1")})
61
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
62
62
  call = await script.next_call()
63
- await drv.command("run/steer", {"message": msg("s1")})
63
+ await drv.command("run/steer", {"message": msg("s1")}, terminal=False)
64
64
  await drv.wait(lambda s: len(s["steerQueue"]) == 1)
65
65
  assert drv.replica["steerQueue"][0]["caller"] == {"clientId": "c1"}
66
66
  [taken] = call.ctx.take_steered()
@@ -71,19 +71,19 @@ async def test_take_steered_strips_the_caller_stamp():
71
71
  async def test_max_queued_caps_each_lane():
72
72
  script = Script()
73
73
  async with run_host(script, max_queued=1) as (drv, _):
74
- await drv.command("run/enqueue", {"message": msg("m1")})
74
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
75
75
  call = await script.next_call()
76
- assert (await drv.command("run/enqueue", {"message": msg("m2")}))[
76
+ assert (await drv.command("run/enqueue", {"message": msg("m2")}, terminal=False))[
77
77
  "type"
78
- ] == "accepted"
79
- full = await drv.command("run/enqueue", {"message": msg("m3")})
78
+ ] == "pending"
79
+ full = await drv.command("run/enqueue", {"message": msg("m3")}, terminal=False)
80
80
  assert full["type"] == "rejected"
81
81
  assert full["payload"] == {"reason": "queue-full"}
82
82
  # The steer lane has its own budget.
83
- assert (await drv.command("run/steer", {"message": msg("s1")}))[
83
+ assert (await drv.command("run/steer", {"message": msg("s1")}, terminal=False))[
84
84
  "type"
85
- ] == "accepted"
86
- steer_full = await drv.command("run/steer", {"message": msg("s2")})
85
+ ] == "pending"
86
+ steer_full = await drv.command("run/steer", {"message": msg("s2")}, terminal=False)
87
87
  assert steer_full["payload"] == {"reason": "queue-full"}
88
88
  call.finish(RunManager.Complete())
89
89
 
@@ -91,11 +91,11 @@ async def test_max_queued_caps_each_lane():
91
91
  async def test_lane_change_into_a_full_lane_rejects():
92
92
  script = Script()
93
93
  async with run_host(script, max_queued=1) as (drv, _):
94
- await drv.command("run/enqueue", {"message": msg("m1")})
94
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
95
95
  call = await script.next_call()
96
- await drv.command("run/enqueue", {"message": msg("m2")})
97
- await drv.command("run/steer", {"message": msg("s1")})
98
- moved = await drv.command("run/steer", {"messageId": "m2"})
96
+ await drv.command("run/enqueue", {"message": msg("m2")}, terminal=False)
97
+ await drv.command("run/steer", {"message": msg("s1")}, terminal=False)
98
+ moved = await drv.command("run/steer", {"messageId": "m2"}, terminal=False)
99
99
  assert moved["payload"] == {"reason": "queue-full"}
100
100
  call.finish(RunManager.Complete())
101
101
 
@@ -10,7 +10,7 @@ from harness_sdk import RunManager
10
10
  async def test_edit_of_dispatched_item_stops_rewinds_reruns(command):
11
11
  script = Script()
12
12
  async with run_host(script, capabilities=("rewind", "rewind-during-run")) as (drv, host):
13
- await drv.command("run/enqueue", {"message": msg("m1")})
13
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
14
14
  call = await script.next_call()
15
15
  script.thread["m1"] = {"parentId": "p0", "role": "user", "isLeaf": True}
16
16
  pending = await drv.command(
@@ -25,20 +25,21 @@ async def test_edit_of_dispatched_item_stops_rewinds_reruns(command):
25
25
  assert [m["parts"][0]["text"] for m in rerun.ctx.messages] == ["edited"]
26
26
  assert rerun.ctx.has_rollback
27
27
  assert rerun.ctx.rollback_to == "p0"
28
+ await drv.wait_status("running")
29
+ rerun.finish(RunManager.Stop(dispatch_queue=False))
28
30
  settled = await drv.res(pending["seq"])
29
31
  assert settled["type"] == "accepted"
30
- await drv.wait_status("running")
31
32
 
32
33
 
33
34
  @pytest.mark.parametrize("capabilities", [(), ("rewind",)])
34
35
  async def test_edit_of_dispatched_item_requires_rewind_during_run(capabilities):
35
36
  script = Script()
36
37
  async with run_host(script, capabilities=capabilities) as (drv, host):
37
- await drv.command("run/enqueue", {"message": msg("m1")})
38
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
38
39
  await script.next_call()
39
40
  script.thread["m1"] = {"parentId": None, "role": "user", "isLeaf": True}
40
41
  assert_rejected(
41
- await drv.command("run/enqueue", {"message": msg("m1", "edited")}),
42
+ await drv.command("run/enqueue", {"message": msg("m1", "edited")}, terminal=False),
42
43
  "capability-missing",
43
44
  )
44
45
 
@@ -47,7 +48,7 @@ async def test_edit_of_dispatched_item_requires_rewind_during_run(capabilities):
47
48
  async def test_edit_of_dispatched_non_leaf_rejects(command):
48
49
  script = Script()
49
50
  async with run_host(script, capabilities=("rewind", "rewind-during-run")) as (drv, host):
50
- await drv.command("run/enqueue", {"message": msg("m1")})
51
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
51
52
  await script.next_call()
52
53
  script.thread["m1"] = {"parentId": None, "role": "user", "isLeaf": False}
53
54
  assert_rejected(
@@ -58,10 +59,10 @@ async def test_edit_of_dispatched_non_leaf_rejects(command):
58
59
  async def test_edit_of_dispatched_item_with_queued_items_stops_and_reruns():
59
60
  script = Script()
60
61
  async with run_host(script, capabilities=("rewind", "rewind-during-run")) as (drv, host):
61
- await drv.command("run/enqueue", {"message": msg("m1")})
62
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
62
63
  call = await script.next_call()
63
64
  script.thread["m1"] = {"parentId": None, "role": "user", "isLeaf": True}
64
- await drv.command("run/enqueue", {"message": msg("m2")})
65
+ await drv.command("run/enqueue", {"message": msg("m2")}, terminal=False)
65
66
  pending = await drv.command(
66
67
  "run/enqueue", {"message": msg("m1", "edited")}, terminal=False
67
68
  )
@@ -69,6 +70,7 @@ async def test_edit_of_dispatched_item_with_queued_items_stops_and_reruns():
69
70
  call.finish(RunManager.Stop(dispatch_queue=False))
70
71
  rerun = await script.next_call()
71
72
  assert rerun.ctx.type == "message-edit"
73
+ rerun.finish(RunManager.Stop(dispatch_queue=False))
72
74
  assert (await drv.res(pending["seq"]))["type"] == "accepted"
73
75
  assert [item["id"] for item in drv.replica["queue"]] == ["m2"]
74
76
 
@@ -76,12 +78,12 @@ async def test_edit_of_dispatched_item_with_queued_items_stops_and_reruns():
76
78
  async def test_edit_of_dispatched_item_in_error_reruns_without_stop():
77
79
  script = Script()
78
80
  async with run_host(script, capabilities=("rewind",)) as (drv, host):
79
- await drv.command("run/enqueue", {"message": msg("m1")})
81
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
80
82
  call = await script.next_call()
81
83
  script.thread["m1"] = {"parentId": None, "role": "user", "isLeaf": True}
82
84
  call.fail(RuntimeError("boom"))
83
85
  await drv.wait_status("error")
84
- await drv.command("run/enqueue", {"message": msg("m1", "edited")})
86
+ await drv.command("run/enqueue", {"message": msg("m1", "edited")}, terminal=False)
85
87
  rerun = await script.next_call()
86
88
  assert rerun.ctx.type == "message-edit"
87
89
  assert rerun.ctx.rollback_to is None
@@ -17,9 +17,9 @@ async def test_edit_in_ready_dispatches_message_edit():
17
17
  thread_with_turn(script)
18
18
  async with run_host(script, capabilities=("rewind",)) as (drv, host):
19
19
  res = await drv.command(
20
- "run/edit", {"sourceId": "u1", "message": msg("u2", "v2")}
20
+ "run/edit", {"sourceId": "u1", "message": msg("u2", "v2")}, terminal=False
21
21
  )
22
- assert res["type"] == "accepted"
22
+ assert res["type"] == "pending"
23
23
  call = await script.next_call()
24
24
  assert call.ctx.type == "message-edit"
25
25
  assert [m["id"] for m in call.ctx.messages] == ["u2"]
@@ -32,7 +32,7 @@ async def test_edit_requires_rewind():
32
32
  thread_with_turn(script)
33
33
  async with run_host(script) as (drv, host):
34
34
  assert_rejected(
35
- await drv.command("run/edit", {"sourceId": "u1", "message": msg("u2")}),
35
+ await drv.command("run/edit", {"sourceId": "u1", "message": msg("u2")}, terminal=False),
36
36
  "capability-missing",
37
37
  )
38
38
 
@@ -41,10 +41,10 @@ async def test_edit_while_running_requires_rewind_during_run():
41
41
  script = Script()
42
42
  thread_with_turn(script)
43
43
  async with run_host(script, capabilities=("rewind",)) as (drv, host):
44
- await drv.command("run/enqueue", {"message": msg("m1")})
44
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
45
45
  await script.next_call()
46
46
  assert_rejected(
47
- await drv.command("run/edit", {"sourceId": "u1", "message": msg("u2")}),
47
+ await drv.command("run/edit", {"sourceId": "u1", "message": msg("u2")}, terminal=False),
48
48
  "capability-missing",
49
49
  )
50
50
 
@@ -53,32 +53,32 @@ async def test_deep_edit_rejected_while_queue_non_empty():
53
53
  script = Script()
54
54
  thread_with_turn(script)
55
55
  async with run_host(script, capabilities=("rewind",)) as (drv, host):
56
- await drv.command("run/enqueue", {"message": msg("m1")})
56
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
57
57
  call = await script.next_call()
58
- await drv.command("run/enqueue", {"message": msg("m2")})
58
+ await drv.command("run/enqueue", {"message": msg("m2")}, terminal=False)
59
59
  call.fail(RuntimeError("boom"))
60
60
  await drv.wait_status("error")
61
61
  assert_rejected(
62
- await drv.command("run/edit", {"sourceId": "u1", "message": msg("u2")}),
62
+ await drv.command("run/edit", {"sourceId": "u1", "message": msg("u2")}, terminal=False),
63
63
  "not-leaf",
64
64
  )
65
65
  await drv.command("run/dequeue", {"messageId": "m2"})
66
- res = await drv.command("run/edit", {"sourceId": "u1", "message": msg("u2")})
67
- assert res["type"] == "accepted"
66
+ res = await drv.command("run/edit", {"sourceId": "u1", "message": msg("u2")}, terminal=False)
67
+ assert res["type"] == "pending"
68
68
  assert (await script.next_call()).ctx.type == "message-edit"
69
69
 
70
70
 
71
71
  async def test_leaf_edit_in_error_accepted_with_queue_non_empty():
72
72
  script = Script()
73
73
  async with run_host(script, capabilities=("rewind",)) as (drv, host):
74
- await drv.command("run/enqueue", {"message": msg("m1")})
74
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
75
75
  call = await script.next_call()
76
76
  script.thread["m1"] = {"parentId": None, "role": "user", "isLeaf": True}
77
- await drv.command("run/enqueue", {"message": msg("m2")})
77
+ await drv.command("run/enqueue", {"message": msg("m2")}, terminal=False)
78
78
  call.fail(RuntimeError("boom"))
79
79
  await drv.wait_status("error")
80
- res = await drv.command("run/edit", {"sourceId": "m1", "message": msg("u2")})
81
- assert res["type"] == "accepted"
80
+ res = await drv.command("run/edit", {"sourceId": "m1", "message": msg("u2")}, terminal=False)
81
+ assert res["type"] == "pending"
82
82
  call = await script.next_call()
83
83
  assert call.ctx.type == "message-edit"
84
84
  assert call.ctx.rollback_to is None
@@ -90,15 +90,15 @@ async def test_edit_unknown_source_and_duplicate_replacement():
90
90
  thread_with_turn(script)
91
91
  async with run_host(script, capabilities=("rewind",)) as (drv, host):
92
92
  assert_rejected(
93
- await drv.command("run/edit", {"sourceId": "zz", "message": msg("u2")}),
93
+ await drv.command("run/edit", {"sourceId": "zz", "message": msg("u2")}, terminal=False),
94
94
  "unknown-id",
95
95
  )
96
96
  assert_rejected(
97
- await drv.command("run/edit", {"sourceId": "u1", "message": msg("a1")}),
97
+ await drv.command("run/edit", {"sourceId": "u1", "message": msg("a1")}, terminal=False),
98
98
  "duplicate-id",
99
99
  )
100
- res = await drv.command("run/edit", {"sourceId": "u1", "message": msg("u1", "v2")})
101
- assert res["type"] == "accepted"
100
+ res = await drv.command("run/edit", {"sourceId": "u1", "message": msg("u1", "v2")}, terminal=False)
101
+ assert res["type"] == "pending"
102
102
 
103
103
 
104
104
  async def test_edit_of_assistant_message_requires_assistant_edit():
@@ -106,7 +106,7 @@ async def test_edit_of_assistant_message_requires_assistant_edit():
106
106
  thread_with_turn(script)
107
107
  async with run_host(script, capabilities=("rewind",)) as (drv, host):
108
108
  assert_rejected(
109
- await drv.command("run/edit", {"sourceId": "a1", "message": msg("u2")}),
109
+ await drv.command("run/edit", {"sourceId": "a1", "message": msg("u2")}, terminal=False),
110
110
  "capability-missing",
111
111
  )
112
112
  script = Script()
@@ -115,8 +115,8 @@ async def test_edit_of_assistant_message_requires_assistant_edit():
115
115
  drv,
116
116
  host,
117
117
  ):
118
- res = await drv.command("run/edit", {"sourceId": "a1", "message": msg("u2")})
119
- assert res["type"] == "accepted"
118
+ res = await drv.command("run/edit", {"sourceId": "a1", "message": msg("u2")}, terminal=False)
119
+ assert res["type"] == "pending"
120
120
  call = await script.next_call()
121
121
  assert call.ctx.rollback_to == "u1"
122
122
 
@@ -125,8 +125,8 @@ 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"})
129
- assert res["type"] == "accepted"
128
+ res = await drv.command("run/reload", {"sourceId": "a1"}, terminal=False)
129
+ assert res["type"] == "pending"
130
130
  call = await script.next_call()
131
131
  assert call.ctx.type == "message-reload"
132
132
  assert call.ctx.messages == ()
@@ -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"}), "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"}), "unknown-id"
146
+ await drv.command("run/reload", {"sourceId": "zz"}, terminal=False), "unknown-id"
147
147
  )
148
- await drv.command("run/enqueue", {"message": msg("m1")})
148
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
149
149
  await script.next_call()
150
150
  assert_rejected(
151
- await drv.command("run/reload", {"sourceId": "a1"}), "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"}), "capability-missing"
160
+ await drv.command("run/reload", {"sourceId": "a1"}, terminal=False), "capability-missing"
161
161
  )
162
162
 
163
163
 
@@ -170,18 +170,18 @@ async def test_reload_leaf_only_while_queue_non_empty():
170
170
  }
171
171
  )
172
172
  async with run_host(script, capabilities=("rewind",)) as (drv, host):
173
- await drv.command("run/enqueue", {"message": msg("m1")})
173
+ await drv.command("run/enqueue", {"message": msg("m1")}, terminal=False)
174
174
  call = await script.next_call()
175
175
  script.thread["m1"] = {"parentId": "a0", "role": "user", "isLeaf": False}
176
176
  script.thread["a1"] = {"parentId": "m1", "role": "assistant", "isLeaf": True}
177
- await drv.command("run/enqueue", {"message": msg("m2")})
177
+ await drv.command("run/enqueue", {"message": msg("m2")}, terminal=False)
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"}), "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"})
184
- assert res["type"] == "accepted"
183
+ res = await drv.command("run/reload", {"sourceId": "a1"}, terminal=False)
184
+ assert res["type"] == "pending"
185
185
  call = await script.next_call()
186
186
  assert call.ctx.rollback_to == "m1"
187
187
 
@@ -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"}), "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"})
206
- assert res["type"] == "accepted"
205
+ res = await drv.command("run/reload", {"sourceId": "a2"}, terminal=False)
206
+ assert res["type"] == "pending"
207
207
  assert (await script.next_call()).ctx.rollback_to == "a1"