harness-sdk-python 0.33.0__tar.gz → 0.35.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 (45) hide show
  1. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/.gitignore +3 -0
  2. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/PKG-INFO +6 -4
  3. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/README.md +4 -2
  4. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/examples/runs_app.py +23 -17
  5. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/pyproject.toml +2 -2
  6. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/src/harness_sdk/run_manager.py +46 -59
  7. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_mint_concurrency.py +6 -5
  8. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/examples/__init__.py +0 -0
  9. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/src/harness_sdk/__init__.py +0 -0
  10. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/src/harness_sdk/checkpoints.py +0 -0
  11. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/src/harness_sdk/deepagents.py +0 -0
  12. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/src/harness_sdk/fenced_postgres.py +0 -0
  13. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/src/harness_sdk/linear_thread.py +0 -0
  14. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/src/harness_sdk/voice.py +0 -0
  15. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/run_helpers.py +0 -0
  16. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_admit_steer.py +0 -0
  17. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_applied_visibility.py +0 -0
  18. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_batches.py +0 -0
  19. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_branch_anchor.py +0 -0
  20. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_checkpoints.py +0 -0
  21. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_deepagents_messages.py +0 -0
  22. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_dispatching.py +0 -0
  23. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_durability.py +0 -0
  24. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_edit_dispatched.py +0 -0
  25. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_edit_reload.py +0 -0
  26. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_enqueue.py +0 -0
  27. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_fenced_postgres.py +0 -0
  28. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_input_required.py +0 -0
  29. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_linear_thread.py +0 -0
  30. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_meta.py +0 -0
  31. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_outcomes.py +0 -0
  32. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_placement.py +0 -0
  33. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_prepare_hooks.py +0 -0
  34. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_restore.py +0 -0
  35. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_rewind_during_run.py +0 -0
  36. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_run_crash_sweep.py +0 -0
  37. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_run_ownership.py +0 -0
  38. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_settle.py +0 -0
  39. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_steer.py +0 -0
  40. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_stop_continue.py +0 -0
  41. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_stopping_window.py +0 -0
  42. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_voice.py +0 -0
  43. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_voice_delegation.py +0 -0
  44. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/test_voice_frontend_tools.py +0 -0
  45. {harness_sdk_python-0.33.0 → harness_sdk_python-0.35.0}/tests/voice_helpers.py +0 -0
@@ -25,3 +25,6 @@ apps/docs/.docs
25
25
  /.deepsec/
26
26
  .vercel
27
27
  .env*
28
+
29
+ MAINTAIN_EPOCH.md
30
+ DOCS_EPOCH.md
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: harness-sdk-python
3
- Version: 0.33.0
3
+ Version: 0.35.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
7
7
  Requires-Python: <4.0,>=3.12
8
8
  Requires-Dist: pinned<0.16,>=0.15.0
9
- Requires-Dist: statewire<0.14,>=0.13.0
9
+ Requires-Dist: statewire<0.16,>=0.15.0
10
10
  Provides-Extra: deepagents
11
11
  Requires-Dist: deepagents>=0.6.12; extra == 'deepagents'
12
12
  Requires-Dist: langchain-core>=0.3; extra == 'deepagents'
@@ -50,8 +50,10 @@ class RunsHost(Statewire):
50
50
  return RunManager.Complete()
51
51
 
52
52
  @command("run/enqueue")
53
- async def run_enqueue(self, params, *, ctx):
54
- return await (await self.run.get()).enqueue(params, applied=ctx.applied)
53
+ async def run_enqueue(self, params, *, ctx, auth_result):
54
+ return await self.run.enqueue(
55
+ params, applied=ctx.applied, auth_result=auth_result
56
+ )
55
57
  ```
56
58
 
57
59
  ## Docs
@@ -31,8 +31,10 @@ class RunsHost(Statewire):
31
31
  return RunManager.Complete()
32
32
 
33
33
  @command("run/enqueue")
34
- async def run_enqueue(self, params, *, ctx):
35
- return await (await self.run.get()).enqueue(params, applied=ctx.applied)
34
+ async def run_enqueue(self, params, *, ctx, auth_result):
35
+ return await self.run.enqueue(
36
+ params, applied=ctx.applied, auth_result=auth_result
37
+ )
36
38
  ```
37
39
 
38
40
  ## Docs
@@ -20,7 +20,7 @@ from typing import Any
20
20
  from fastapi import FastAPI
21
21
  from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
22
22
  from pinned import PinnedHost, PinnedWorker
23
- from pinned.testing import MemoryStateStore
23
+ from pinned.durability import MemoryStateStore
24
24
  from statewire import Statewire, command, plain
25
25
  from statewire.langgraph import append_langgraph_event
26
26
 
@@ -154,36 +154,42 @@ class RunsHost(Statewire):
154
154
  await stream.aclose()
155
155
 
156
156
  @command("run/enqueue")
157
- async def run_enqueue(self, params, *, ctx):
158
- return await (await self.run.get()).enqueue(params, applied=ctx.applied)
157
+ async def run_enqueue(self, params, *, ctx, auth_result):
158
+ return await self.run.enqueue(
159
+ params, applied=ctx.applied, auth_result=auth_result
160
+ )
159
161
 
160
162
  @command("run/steer")
161
- async def run_steer(self, params, *, ctx):
162
- return await (await self.run.get()).steer(params, applied=ctx.applied)
163
+ async def run_steer(self, params, *, ctx, auth_result):
164
+ return await self.run.steer(
165
+ params, applied=ctx.applied, auth_result=auth_result
166
+ )
163
167
 
164
168
  @command("run/dequeue")
165
- async def run_dequeue(self, params):
166
- return await (await self.run.get()).dequeue(params)
169
+ async def run_dequeue(self, params, *, auth_result):
170
+ return await self.run.dequeue(params, auth_result=auth_result)
167
171
 
168
172
  @command("run/edit")
169
- async def run_edit(self, params, *, ctx):
170
- return await (await self.run.get()).edit(params, applied=ctx.applied)
173
+ async def run_edit(self, params, *, ctx, auth_result):
174
+ return await self.run.edit(params, applied=ctx.applied, auth_result=auth_result)
171
175
 
172
176
  @command("run/reload")
173
- async def run_reload(self, params, *, ctx):
174
- return await (await self.run.get()).reload(params, applied=ctx.applied)
177
+ async def run_reload(self, params, *, ctx, auth_result):
178
+ return await self.run.reload(
179
+ params, applied=ctx.applied, auth_result=auth_result
180
+ )
175
181
 
176
182
  @command("run/stop")
177
- async def run_stop(self, params=None, *, ctx):
178
- return await (await self.run.get()).stop(params, applied=ctx.applied)
183
+ async def run_stop(self, params=None, *, ctx, auth_result):
184
+ return await self.run.stop(params, applied=ctx.applied, auth_result=auth_result)
179
185
 
180
186
  @command("run/continue")
181
- async def run_continue(self, params=None, *, ctx):
182
- return await (await self.run.get()).continue_run(applied=ctx.applied)
187
+ async def run_continue(self, params=None, *, ctx, auth_result):
188
+ return await self.run.continue_run(applied=ctx.applied, auth_result=auth_result)
183
189
 
184
190
  @command("run/input")
185
- async def run_input(self, params):
186
- return await (await self.run.get()).input(params)
191
+ async def run_input(self, params, *, auth_result):
192
+ return await self.run.input(params, auth_result=auth_result)
187
193
 
188
194
 
189
195
  worker = PinnedWorker(
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "harness-sdk-python"
3
- version = "0.33.0"
3
+ version = "0.35.0"
4
4
  description = "RunManager: the harness-sdk runs subsystem for Python Statewire hosts"
5
5
  readme = "README.md"
6
6
  license = "MIT"
7
7
  requires-python = ">=3.12,<4.0"
8
- dependencies = ["statewire>=0.13.0,<0.14", "pinned>=0.15.0,<0.16"]
8
+ dependencies = ["statewire>=0.15.0,<0.16", "pinned>=0.15.0,<0.16"]
9
9
 
10
10
  [project.optional-dependencies]
11
11
  postgres = ["langgraph-checkpoint-postgres>=2.0.0"]
@@ -63,7 +63,6 @@ equality per principal.
63
63
  """
64
64
 
65
65
  import asyncio
66
- import contextlib
67
66
  import logging
68
67
  from dataclasses import dataclass, field
69
68
  from typing import Any, AsyncIterator, Callable, Coroutine, Iterable, Protocol
@@ -220,6 +219,17 @@ def _validated_response(
220
219
  return response
221
220
 
222
221
 
222
+ def _check_rewind_gate(caps: Any, in_flight: bool, command: str) -> None:
223
+ if in_flight:
224
+ if "rewind-during-run" not in caps:
225
+ raise _reject(
226
+ "capability-missing",
227
+ f"{command} during a run requires the rewind-during-run capability",
228
+ )
229
+ elif "rewind" not in caps:
230
+ raise _reject("capability-missing", "the rewind capability is not enabled")
231
+
232
+
223
233
  def _check_rewind_anchor(params: Any, source_meta: dict[str, Any]) -> None:
224
234
  anchor = params.get("runAnchorMessageId", _ABSENT)
225
235
  if anchor is _ABSENT:
@@ -304,8 +314,9 @@ class _Continue:
304
314
  @dataclass
305
315
  class _Input:
306
316
  request_id: str
307
- response: dict[str, Any]
317
+ response: Any
308
318
  meta: Any
319
+ principal: Any
309
320
  future: "asyncio.Future[Any]" = field(default_factory=_future)
310
321
 
311
322
 
@@ -1093,10 +1104,9 @@ class RunManager(Tray):
1093
1104
  raise _reject("queue-full", f"queue is full ({self.max_queued} messages)")
1094
1105
  self._batch.extend(("steerQueue", item) for item in steer)
1095
1106
  self._batch.append((e.lane, self._stamped(e.message, e.meta)))
1096
- self.dispatching.value = {
1097
- "trigger": self._send_trigger(),
1098
- "messages": [_public(item) for _, item in self._batch],
1099
- }
1107
+ self.dispatching.value = _public_record(
1108
+ self._record_of(self._send_trigger(), [item for _, item in self._batch])
1109
+ )
1100
1110
 
1101
1111
  def _dispatch_staged(
1102
1112
  self, *, input_outcomes: tuple[tuple[dict[str, Any], Any, Any], ...] = ()
@@ -1331,14 +1341,7 @@ class RunManager(Tray):
1331
1341
  def _park_dispatched_edit(self, e: _Send) -> Any:
1332
1342
  assert e.message is not None
1333
1343
  self._check_run_owner(e.principal)
1334
- if self._in_flight():
1335
- if "rewind-during-run" not in self.capabilities:
1336
- raise _reject(
1337
- "capability-missing",
1338
- "editing during a run requires the rewind-during-run capability",
1339
- )
1340
- elif "rewind" not in self.capabilities:
1341
- raise _reject("capability-missing", "the rewind capability is not enabled")
1344
+ _check_rewind_gate(self.capabilities, self._in_flight(), "editing")
1342
1345
  if e.source_meta is None:
1343
1346
  raise _reject("unknown-id", f"message {e.message_id} is unknown")
1344
1347
  self._check_anchor(e)
@@ -1367,6 +1370,7 @@ class RunManager(Tray):
1367
1370
  def _apply_input(self, e: _Input) -> Any:
1368
1371
  if self._status() != "input-required":
1369
1372
  raise _reject("wrong-state", f"run/input is rejected in {self._status()}")
1373
+ self._check_run_owner(e.principal)
1370
1374
  index = next(
1371
1375
  (
1372
1376
  i
@@ -1382,9 +1386,12 @@ class RunManager(Tray):
1382
1386
  "already-answered",
1383
1387
  f"input request {e.request_id} is already answered",
1384
1388
  )
1385
- self._input_answers[e.request_id] = (e.response, e.meta)
1389
+ response = _validated_response(
1390
+ type(self), self._input_requests[index]["type"], e.response
1391
+ )
1392
+ self._input_answers[e.request_id] = (response, e.meta)
1386
1393
  requests = self.inputRequests.value
1387
- requests[index]["response"] = e.response
1394
+ requests[index]["response"] = response
1388
1395
  if e.meta is not None:
1389
1396
  requests[index]["meta"] = e.meta
1390
1397
  self.inputRequests.value = requests
@@ -1471,19 +1478,9 @@ class RunManager(Tray):
1471
1478
  async def input(
1472
1479
  self, params: dict[str, Any], *, meta: Any = None, auth_result: Any
1473
1480
  ) -> Any:
1474
- request_id = params["requestId"]
1475
- if self._status() != "input-required":
1476
- raise _reject("wrong-state", f"run/input is rejected in {self._status()}")
1477
- self._check_run_owner(auth_result)
1478
- request = next((r for r in self._input_requests if r["id"] == request_id), None)
1479
- if request is None:
1480
- raise _reject("unknown-id", f"input request {request_id} is unknown")
1481
- if request_id in self._input_answers:
1482
- raise _reject(
1483
- "already-answered", f"input request {request_id} is already answered"
1484
- )
1485
- response = _validated_response(type(self), request["type"], params["response"])
1486
- return await self._stage(_Input(request_id, response, meta))
1481
+ return await self._stage(
1482
+ _Input(params["requestId"], params["response"], meta, auth_result)
1483
+ )
1487
1484
 
1488
1485
  async def continue_run(
1489
1486
  self, *, meta: Any = None, applied: Callable[[], None], auth_result: Any
@@ -1564,10 +1561,9 @@ class RunManager(Tray):
1564
1561
  added = [item["id"] for item in items if item["id"] not in known]
1565
1562
  if added:
1566
1563
  manager.dispatchedIds.value = [*known, *added]
1567
- manager.dispatching.value = {
1568
- "trigger": "steer",
1569
- "messages": [_public(item) for item in items],
1570
- }
1564
+ manager.dispatching.value = _public_record(
1565
+ manager._record_of("steer", items)
1566
+ )
1571
1567
  self._ctx._message_meta.update(
1572
1568
  {item["id"]: item.get("meta") for item in items}
1573
1569
  )
@@ -1705,23 +1701,20 @@ class RunManagerNamespace(TrayMapNamespace):
1705
1701
  async def _machine(self, run_id: str, principal: Any) -> tuple[RunManager, bool]:
1706
1702
  # No await between the live() check and the mint's publication, and no
1707
1703
  # lock held across storage I/O: same-tick callers await the shared mint
1708
- # task and merge into the machine it mounts; the creator raises its
1709
- # mint's failure while followers re-mint and progress.
1710
- while True:
1711
- tray = self.live()
1712
- if tray is not None:
1713
- return tray, False
1714
- pending = self._mint
1715
- if pending is None:
1716
- pending = self._api.create_task(self.get_or_create(run_id))
1717
- self._mint = pending
1718
- pending.add_done_callback(self._mint_settled)
1719
- tray = await pending
1720
- if principal is not None and tray.principal.value is None:
1721
- tray.principal.value = principal
1722
- return tray, True
1723
- with contextlib.suppress(Exception):
1724
- await pending
1704
+ # task and merge into the machine it mounts, or raise its failure.
1705
+ tray = self.live()
1706
+ if tray is not None:
1707
+ return tray, False
1708
+ pending = self._mint
1709
+ minted = pending is None
1710
+ if pending is None:
1711
+ pending = self._api.create_task(self.get_or_create(run_id))
1712
+ self._mint = pending
1713
+ pending.add_done_callback(self._mint_settled)
1714
+ tray = await pending
1715
+ if minted and principal is not None and tray.principal.value is None:
1716
+ tray.principal.value = principal
1717
+ return tray, minted
1725
1718
 
1726
1719
  def _mint_settled(self, task: "asyncio.Task[Any]") -> None:
1727
1720
  if self._mint is task:
@@ -1876,15 +1869,9 @@ class RunManagerNamespace(TrayMapNamespace):
1876
1869
  def _check_rewind_gate(self, command: str) -> None:
1877
1870
  cls: type[RunManager] = self._spec.tray_cls # type: ignore[assignment]
1878
1871
  live = self.live()
1879
- status = "ready" if live is None else live._status()
1880
- if status in ("running", "stopping"):
1881
- if "rewind-during-run" not in cls.capabilities:
1882
- raise _reject(
1883
- "capability-missing",
1884
- f"{command} during a run requires the rewind-during-run capability",
1885
- )
1886
- elif "rewind" not in cls.capabilities:
1887
- raise _reject("capability-missing", "the rewind capability is not enabled")
1872
+ _check_rewind_gate(
1873
+ cls.capabilities, live is not None and live._in_flight(), command
1874
+ )
1888
1875
 
1889
1876
  async def edit(
1890
1877
  self,
@@ -27,10 +27,10 @@ class WedgeStore(MemoryStateStore):
27
27
  return await super().get_tray(ns, id, tray_ns, tray_id, epoch)
28
28
 
29
29
 
30
- async def test_a_parked_mint_fails_at_the_deadline_and_followers_progress():
30
+ async def test_a_parked_mint_failure_reaches_the_creator_and_its_followers():
31
31
  """No lock is held across mint storage I/O: a wedged mint fails loudly at
32
- the store deadline, the waiting command re-mints its own machine, and the
33
- wedged command surfaces the store error."""
32
+ the store deadline, and every command that merged into it raises that
33
+ same store error instead of re-minting."""
34
34
  script = Script()
35
35
  store = WedgeStore()
36
36
  async with run_host(script, store=store) as (drv, host):
@@ -42,10 +42,11 @@ async def test_a_parked_mint_fails_at_the_deadline_and_followers_progress():
42
42
  await asyncio.sleep(0.05)
43
43
  assert not follower.done() # same-tick mints merge: followers await the pending mint
44
44
  store.fail()
45
- tray = await asyncio.wait_for(follower, 5)
46
- assert tray.id == "run-b"
45
+ with pytest.raises(TimeoutError):
46
+ await asyncio.wait_for(follower, 5)
47
47
  with pytest.raises(TimeoutError):
48
48
  await parked
49
+ assert list(api.runs.instances) == []
49
50
 
50
51
 
51
52
  async def test_concurrent_same_run_id_mints_resolve_to_one_machine():