harness-sdk-python 0.8.0__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/PKG-INFO +1 -1
  2. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/pyproject.toml +1 -1
  3. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/src/harness_sdk/linear_thread.py +0 -4
  4. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/src/harness_sdk/run_manager.py +4 -30
  5. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/run_helpers.py +0 -4
  6. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_linear_thread.py +2 -11
  7. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_settle.py +0 -2
  8. harness_sdk_python-0.8.0/tests/test_run_leaf.py +0 -191
  9. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/.gitignore +0 -0
  10. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/README.md +0 -0
  11. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/src/harness_sdk/__init__.py +0 -0
  12. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/src/harness_sdk/fenced_postgres.py +0 -0
  13. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_ack_visibility.py +0 -0
  14. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_batches.py +0 -0
  15. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_branch_anchor.py +0 -0
  16. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_dispatching.py +0 -0
  17. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_edit_dispatched.py +0 -0
  18. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_edit_reload.py +0 -0
  19. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_enqueue.py +0 -0
  20. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_facade.py +0 -0
  21. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_fenced_postgres.py +0 -0
  22. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_input_required.py +0 -0
  23. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_meta.py +0 -0
  24. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_outcomes.py +0 -0
  25. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_placement.py +0 -0
  26. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_prepare_hooks.py +0 -0
  27. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_rewind_during_run.py +0 -0
  28. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/tests/test_steer.py +0 -0
  29. {harness_sdk_python-0.8.0 → harness_sdk_python-0.9.0}/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.8.0
3
+ Version: 0.9.0
4
4
  Summary: RunManager: the harness-sdk runs subsystem for Python Statewire hosts
5
5
  Project-URL: Repository, https://github.com/assistant-ui/harness-sdk
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "harness-sdk-python"
3
- version = "0.8.0"
3
+ version = "0.9.0"
4
4
  description = "RunManager: the harness-sdk runs subsystem for Python Statewire hosts"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -21,10 +21,6 @@ class LinearThread:
21
21
  }
22
22
  return None
23
23
 
24
- async def get_leaf_message_id(self) -> str | None:
25
- items = self._messages()
26
- return items[-1].get("id") if items else None
27
-
28
24
  async def get_message_child_id(self, parent_id: str) -> str | None:
29
25
  """The id of the first non-tool message after parent_id; None when parent_id is unknown or only tools follow."""
30
26
  items = self._messages()
@@ -15,9 +15,9 @@ messages, rejected otherwise; ``run/stop`` awaits a future the drain resolves
15
15
  once the in-flight run has ended.
16
16
 
17
17
  ``entry["dispatching"]`` is the dispatch record (or None): the payload the
18
- run callback receives. ``ctx.ack()`` clears it and confirms
19
- ``runLeafMessageId``; ``steering.take()`` may refill it — at most one unacked
20
- batch ever exists. A run that ends with an unacked record reverts its items
18
+ run callback receives. ``ctx.ack()`` clears it; ``steering.take()`` may
19
+ refill it — at most one unacked batch ever exists. A run that ends with an
20
+ unacked record reverts its items
21
21
  by provenance: lane items to the front of their lane, direct dispatches to
22
22
  the front of the queue, rewind replacements dropped.
23
23
  """
@@ -155,10 +155,6 @@ class RunManager:
155
155
  """Meta for a known id ({parentId, role, isLeaf, onActiveBranch}), None for an unknown one; a None id probes the root ({isLeaf})."""
156
156
  ...
157
157
 
158
- async def get_leaf_message_id(self) -> str | None:
159
- """The active branch's current leaf id, None while the thread is empty."""
160
- ...
161
-
162
158
  def __init__(
163
159
  self,
164
160
  *,
@@ -200,7 +196,6 @@ class RunManager:
200
196
  self._staged_rewinds: list[_Rewind] = []
201
197
  self._dispatching: list[tuple[str, dict[str, Any]]] = []
202
198
  self._run_acked = False
203
- self._leaf_confirmed: str | None = None
204
199
  self._run_futures: list["asyncio.Future[Any]"] = []
205
200
  self._input_requests: list[dict[str, Any]] = []
206
201
  self._input_answers: dict[str, Any] = {}
@@ -222,7 +217,6 @@ class RunManager:
222
217
  "queue",
223
218
  "steerQueue",
224
219
  "runId",
225
- "runLeafMessageId",
226
220
  "dispatch",
227
221
  "inputRequests",
228
222
  ):
@@ -247,7 +241,6 @@ class RunManager:
247
241
  "steerQueue": [],
248
242
  "dispatching": None,
249
243
  "error": None,
250
- "runLeafMessageId": None,
251
244
  }
252
245
  )
253
246
  return self._state["runs"][0]
@@ -454,14 +447,10 @@ class RunManager:
454
447
  self._stop_reason = None
455
448
  self._run_acked = False
456
449
  entry = self._ensure_entry()
457
- self._leaf_confirmed = plain(entry["runLeafMessageId"])
458
450
  entry["dispatching"] = record
459
451
  entry["error"] = None
460
452
  if messages:
461
453
  self._dispatched_ids = tuple(m["id"] for m in messages)
462
- if rollback_to is not _ABSENT:
463
- # The true leaf is the not-yet-created message; ack records it.
464
- entry["runLeafMessageId"] = None
465
454
  entry["runId"] = uuid.uuid4().hex
466
455
  self._set_status("running")
467
456
  ctx = RunManager.RunContext(
@@ -496,7 +485,6 @@ class RunManager:
496
485
  f"{type(outcome).__name__}"
497
486
  )
498
487
  except asyncio.CancelledError:
499
- await self._pull_leaf()
500
488
  self._settle(ctx)
501
489
  self._set_status("stopped")
502
490
  self._entry()["runId"] = None
@@ -507,7 +495,6 @@ class RunManager:
507
495
  self._idle.set()
508
496
  raise # no drain, no freeze
509
497
  except Exception as exc:
510
- await self._pull_leaf()
511
498
  self._settle(ctx)
512
499
  message = str(exc) or type(exc).__name__
513
500
  if isinstance(exc, StatewireReject):
@@ -521,7 +508,6 @@ class RunManager:
521
508
  self._revert_dispatching()
522
509
  self._drain()
523
510
  return
524
- await self._pull_leaf()
525
511
  self._settle(ctx)
526
512
  if self._run_acked:
527
513
  error = None
@@ -536,12 +522,6 @@ class RunManager:
536
522
  self._outcome = outcome
537
523
  self._drain()
538
524
 
539
- async def _pull_leaf(self) -> None:
540
- # An end with an unacked batch reverts instead of recording a leaf.
541
- if plain(self._entry()["dispatching"]) is not None:
542
- return
543
- self._entry()["runLeafMessageId"] = await self._thread.get_leaf_message_id()
544
-
545
525
  def _settle(self, ctx: "RunManager.RunContext") -> None:
546
526
  if self._ctx is ctx:
547
527
  self._ctx = None
@@ -566,17 +546,11 @@ class RunManager:
566
546
  self._dispatching = []
567
547
  entry["dispatching"] = None
568
548
  self._run_acked = True
569
- if record["messages"]:
570
- entry["runLeafMessageId"] = record["messages"][-1]["id"]
571
- self._leaf_confirmed = plain(entry["runLeafMessageId"])
572
549
 
573
550
  def _revert_dispatching(self) -> None:
574
551
  taken, self._dispatching = self._dispatching, []
575
552
  entry = self._ensure_entry()
576
- record = plain(entry["dispatching"])
577
553
  entry["dispatching"] = None
578
- if record is not None and "rollbackTo" in record:
579
- entry["runLeafMessageId"] = self._leaf_confirmed
580
554
  if not taken:
581
555
  return
582
556
  for lane in ("steerQueue", "queue"):
@@ -1319,7 +1293,7 @@ class RunManager:
1319
1293
  raise RuntimeError("this run has already settled")
1320
1294
 
1321
1295
  def ack(self) -> None:
1322
- """Ack the current batch: clears the dispatch record and confirms the run leaf."""
1296
+ """Ack the current batch: clears the dispatch record."""
1323
1297
  self._ensure_active()
1324
1298
  self._manager._ack()
1325
1299
 
@@ -32,7 +32,6 @@ class Script:
32
32
  def __init__(self) -> None:
33
33
  self.calls: asyncio.Queue[Call] = asyncio.Queue()
34
34
  self.thread: dict[str, dict[str, Any]] = {}
35
- self.leaf: str | None = None
36
35
 
37
36
  async def run(self, ctx: RunManager.RunContext) -> Any:
38
37
  call = Call(ctx, asyncio.get_running_loop().create_future())
@@ -44,9 +43,6 @@ class Script:
44
43
  return {"isLeaf": not self.thread}
45
44
  return self.thread.get(message_id)
46
45
 
47
- async def get_leaf_message_id(self) -> str | None:
48
- return self.leaf
49
-
50
46
  async def next_call(self, timeout: float = 5) -> Call:
51
47
  return await asyncio.wait_for(self.calls.get(), timeout)
52
48
 
@@ -1,8 +1,8 @@
1
1
  """Contract: ``linear_thread`` projects a linear message list for RunManager —
2
2
  ``get_message_meta(None)`` probes the root (isLeaf iff empty), a known id gets
3
3
  ``{parentId, role, isLeaf, onActiveBranch}`` chained by list order, an unknown
4
- id gets ``None``; ``get_leaf_message_id`` is the last message's id; the extra
5
- ``get_message_child_id`` resolves the first non-tool successor."""
4
+ id gets ``None``; the extra ``get_message_child_id`` resolves the first
5
+ non-tool successor."""
6
6
 
7
7
  import pytest
8
8
 
@@ -69,14 +69,6 @@ async def test_unknown_id_returns_none():
69
69
  assert await thread.get_message_meta("nope") is None
70
70
 
71
71
 
72
- async def test_leaf_is_the_last_message_id():
73
- assert await _thread_for(THREAD).get_leaf_message_id() == "u2"
74
-
75
-
76
- async def test_leaf_of_an_empty_thread_is_none():
77
- assert await _thread_for([]).get_leaf_message_id() is None
78
-
79
-
80
72
  async def test_child_id_skips_tool_messages():
81
73
  thread = _thread_for(
82
74
  [
@@ -109,7 +101,6 @@ async def test_reads_the_live_list():
109
101
  assert await thread.get_message_meta(None) == {"isLeaf": True}
110
102
  messages.append({"id": "u1", "type": "human"})
111
103
  assert await thread.get_message_meta(None) == {"isLeaf": False}
112
- assert await thread.get_leaf_message_id() == "u1"
113
104
 
114
105
 
115
106
  def test_non_callable_messages_raises():
@@ -101,7 +101,6 @@ async def test_unacked_run_end_reverts_entries_to_the_lane_front():
101
101
  await drv.wait_status("error")
102
102
  assert queue_ids(drv.replica, "queue") == ["m1", "m2"]
103
103
  assert drv.run["error"] == {"message": "boom"}
104
- assert drv.run["runLeafMessageId"] is None
105
104
  await drv.command("run/steer", {"messageId": "m1"}, terminal=False)
106
105
  continued = await script.next_call()
107
106
  assert continued.ctx.trigger == "error-continue"
@@ -211,7 +210,6 @@ async def test_task_cancellation_reverts_entries_and_settles_sends():
211
210
  assert (await drv.res(seq))["type"] == "accepted"
212
211
  await drv.wait_status("stopped")
213
212
  assert queue_ids(drv.replica, "queue") == ["m1", "m2"]
214
- assert drv.run["runLeafMessageId"] is None
215
213
  assert task.cancelled()
216
214
 
217
215
 
@@ -1,191 +0,0 @@
1
- from run_helpers import Script, add, msg, run_host, run_of
2
-
3
- from harness_sdk import RunManager
4
-
5
-
6
- def thread_with_turn(script: Script) -> None:
7
- script.thread.update(
8
- {
9
- "u1": {
10
- "parentId": None,
11
- "role": "user",
12
- "isLeaf": False,
13
- "onActiveBranch": True,
14
- },
15
- "a1": {
16
- "parentId": "u1",
17
- "role": "assistant",
18
- "isLeaf": True,
19
- "onActiveBranch": True,
20
- },
21
- }
22
- )
23
-
24
-
25
- async def test_ready_state_has_no_run_entry():
26
- script = Script()
27
- thread_with_turn(script)
28
- async with run_host(script) as (drv, host):
29
- assert drv.replica["status"] == "ready"
30
- assert drv.replica["runs"] == []
31
-
32
-
33
- async def test_ack_sets_the_leaf_to_the_last_batch_message():
34
- script = Script()
35
- async with run_host(script) as (drv, host):
36
- drv.post("run/enqueue", add("m1"))
37
- call = await script.next_call()
38
- await drv.wait_status("running")
39
- assert drv.run["runLeafMessageId"] is None
40
- call.ack()
41
- await drv.wait(lambda s: run_of(s).get("runLeafMessageId") == "m1")
42
-
43
-
44
- async def test_steer_batch_ack_sets_the_leaf_to_the_last_lane_item():
45
- script = Script()
46
- async with run_host(script) as (drv, host):
47
- drv.post("run/enqueue", add("m1"))
48
- call = await script.next_call()
49
- call.ack()
50
- await drv.command("run/steer", add("s1", anchor="m1"), terminal=False)
51
- await drv.command("run/steer", add("s2", anchor="s1"), terminal=False)
52
- call.finish(RunManager.Complete())
53
- steered = await script.next_call()
54
- assert steered.ctx.messages[-1]["id"] == "s2"
55
- assert drv.run["runLeafMessageId"] == "m1"
56
- steered.ack()
57
- await drv.wait(lambda s: run_of(s).get("runLeafMessageId") == "s2")
58
-
59
-
60
- async def test_steering_take_moves_the_leaf_at_its_ack():
61
- script = Script()
62
- async with run_host(script) as (drv, host):
63
- drv.post("run/enqueue", add("m1"))
64
- call = await script.next_call()
65
- call.ack()
66
- await drv.command("run/steer", add("s1", anchor="m1"), terminal=False)
67
- await drv.wait(lambda s: len(run_of(s).get("steerQueue", [])) == 1)
68
- call.ctx.steering.take()
69
- assert drv.run["runLeafMessageId"] == "m1"
70
- call.ack()
71
- await drv.wait(lambda s: run_of(s).get("runLeafMessageId") == "s1")
72
- call.finish(RunManager.Complete())
73
-
74
-
75
- async def test_settle_pulls_the_thread_leaf():
76
- for outcome in [
77
- RunManager.Error(dispatch_queue=False),
78
- RunManager.Stop(dispatch_queue=False),
79
- ]:
80
- script = Script()
81
- async with run_host(script) as (drv, host):
82
- drv.post("run/enqueue", add("m1"))
83
- call = await script.next_call()
84
- call.ack()
85
- script.leaf = "a1"
86
- call.finish(outcome)
87
- await drv.wait_status("error", "stopped")
88
- assert drv.run["runLeafMessageId"] == "a1"
89
-
90
-
91
- async def test_input_required_settle_pulls_the_thread_leaf():
92
- script = Script()
93
- async with run_host(script) as (drv, host):
94
- drv.post("run/enqueue", add("m1"))
95
- call = await script.next_call()
96
- call.ack()
97
- script.leaf = "a1"
98
- call.finish(
99
- RunManager.InputRequired(
100
- requests=({"id": "r1", "type": "tool-call", "toolCallId": "t1"},)
101
- )
102
- )
103
- await drv.wait_status("input-required")
104
- assert drv.run["runLeafMessageId"] == "a1"
105
-
106
-
107
- async def test_unacked_end_reverts_instead_of_pulling():
108
- script = Script()
109
- async with run_host(script) as (drv, host):
110
- drv.post("run/enqueue", add("m1"))
111
- call = await script.next_call()
112
- script.leaf = "a1"
113
- call.fail(RuntimeError("boom"))
114
- await drv.wait_status("error")
115
- assert drv.run["runLeafMessageId"] is None
116
- assert [item["id"] for item in drv.run["queue"]] == ["m1"]
117
-
118
-
119
- async def test_complete_clears_the_leaf_on_ready():
120
- script = Script()
121
- async with run_host(script) as (drv, host):
122
- drv.post("run/enqueue", add("m1"))
123
- call = await script.next_call()
124
- call.ack()
125
- script.leaf = "a1"
126
- await drv.wait_status("running")
127
- call.finish(RunManager.Complete())
128
- await drv.wait_status("ready")
129
- assert drv.replica["runs"] == []
130
-
131
-
132
- async def test_complete_with_a_queued_item_keeps_the_leaf_flowing():
133
- script = Script()
134
- async with run_host(script) as (drv, host):
135
- drv.post("run/enqueue", add("m1"))
136
- call = await script.next_call()
137
- call.ack()
138
- await drv.command("run/enqueue", add("m2", anchor="m1"), terminal=False)
139
- script.leaf = "a1"
140
- call.finish(RunManager.Complete())
141
- drain = await script.next_call()
142
- drain.ack()
143
- await drv.wait(lambda s: run_of(s).get("runLeafMessageId") == "m2")
144
- drain.finish(RunManager.Complete())
145
- await drv.wait_status("ready")
146
- assert drv.replica["runs"] == []
147
-
148
-
149
- async def test_rewind_dispatch_nulls_the_leaf_and_the_ack_records_the_replacement():
150
- script = Script()
151
- thread_with_turn(script)
152
- async with run_host(script, capabilities=("rewind",)) as (drv, host):
153
- drv.post("run/edit", {"sourceId": "u1", "message": msg("u2")})
154
- call = await script.next_call()
155
- await drv.wait_status("running")
156
- assert drv.run["runLeafMessageId"] is None
157
- call.ack()
158
- await drv.wait(lambda s: run_of(s).get("runLeafMessageId") == "u2")
159
-
160
-
161
- async def test_reload_dispatch_nulls_the_leaf_until_the_settle_pull():
162
- script = Script()
163
- thread_with_turn(script)
164
- async with run_host(script, capabilities=("rewind",)) as (drv, host):
165
- drv.post("run/reload", {"sourceId": "a1"})
166
- call = await script.next_call()
167
- await drv.wait_status("running")
168
- assert drv.run["runLeafMessageId"] is None
169
- call.ack()
170
- assert drv.run["runLeafMessageId"] is None
171
- script.leaf = "a2"
172
- call.finish(RunManager.Stop(dispatch_queue=False))
173
- await drv.wait(lambda s: run_of(s).get("runLeafMessageId") == "a2")
174
-
175
-
176
- async def test_unacked_rewind_end_restores_the_previous_leaf():
177
- script = Script()
178
- thread_with_turn(script)
179
- async with run_host(script, capabilities=("rewind",)) as (drv, host):
180
- drv.post("run/enqueue", add("m1", anchor="a1"))
181
- call = await script.next_call()
182
- call.ack()
183
- script.leaf = "a2"
184
- call.finish(RunManager.Stop(dispatch_queue=False))
185
- await drv.wait(lambda s: run_of(s).get("runLeafMessageId") == "a2")
186
- drv.post("run/edit", {"sourceId": "u1", "message": msg("u2")})
187
- rerun = await script.next_call()
188
- await drv.wait(lambda s: run_of(s).get("runLeafMessageId") is None)
189
- rerun.fail(RuntimeError("boom"))
190
- await drv.wait_status("error")
191
- assert drv.run["runLeafMessageId"] == "a2"