harness-sdk-python 0.7.0__tar.gz → 0.7.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 (29) hide show
  1. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/.gitignore +1 -0
  2. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/PKG-INFO +6 -1
  3. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/pyproject.toml +7 -1
  4. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/src/harness_sdk/run_manager.py +7 -1
  5. harness_sdk_python-0.7.1/tests/test_dispatching.py +123 -0
  6. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/README.md +0 -0
  7. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/src/harness_sdk/__init__.py +0 -0
  8. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/src/harness_sdk/fenced_postgres.py +0 -0
  9. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/src/harness_sdk/linear_thread.py +0 -0
  10. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/run_helpers.py +0 -0
  11. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_ack_visibility.py +0 -0
  12. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_batches.py +0 -0
  13. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_branch_anchor.py +0 -0
  14. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_edit_dispatched.py +0 -0
  15. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_edit_reload.py +0 -0
  16. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_enqueue.py +0 -0
  17. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_facade.py +0 -0
  18. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_fenced_postgres.py +0 -0
  19. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_input_required.py +0 -0
  20. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_linear_thread.py +0 -0
  21. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_meta.py +0 -0
  22. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_outcomes.py +0 -0
  23. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_placement.py +0 -0
  24. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_prepare_hooks.py +0 -0
  25. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_rewind_during_run.py +0 -0
  26. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_run_leaf.py +0 -0
  27. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_settle.py +0 -0
  28. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_steer.py +0 -0
  29. {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_stop_continue.py +0 -0
@@ -21,3 +21,4 @@ __pycache__
21
21
  /d[0-9]*.md
22
22
  /doc_*.md
23
23
  apps/docs/.docs
24
+ /.agentdoc/
@@ -1,11 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: harness-sdk-python
3
- Version: 0.7.0
3
+ Version: 0.7.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
7
7
  Requires-Python: <4.0,>=3.11
8
8
  Requires-Dist: statewire<0.5,>=0.4.0
9
+ Provides-Extra: deepagents
10
+ Requires-Dist: deepagents>=0.6.12; extra == 'deepagents'
11
+ Requires-Dist: langchain-core>=0.3; extra == 'deepagents'
12
+ Requires-Dist: langgraph>=1.0.0; extra == 'deepagents'
13
+ Requires-Dist: uvicorn[standard]>=0.30.0; extra == 'deepagents'
9
14
  Provides-Extra: postgres
10
15
  Requires-Dist: langgraph-checkpoint-postgres>=2.0.0; extra == 'postgres'
11
16
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "harness-sdk-python"
3
- version = "0.7.0"
3
+ version = "0.7.1"
4
4
  description = "RunManager: the harness-sdk runs subsystem for Python Statewire hosts"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -9,6 +9,12 @@ dependencies = ["statewire>=0.4.0,<0.5"]
9
9
 
10
10
  [project.optional-dependencies]
11
11
  postgres = ["langgraph-checkpoint-postgres>=2.0.0"]
12
+ deepagents = [
13
+ "deepagents>=0.6.12",
14
+ "langgraph>=1.0.0",
15
+ "langchain-core>=0.3",
16
+ "uvicorn[standard]>=0.30.0",
17
+ ]
12
18
 
13
19
  [project.urls]
14
20
  Repository = "https://github.com/assistant-ui/harness-sdk"
@@ -244,6 +244,7 @@ class RunManager:
244
244
  "status": "ready",
245
245
  "queue": [],
246
246
  "steerQueue": [],
247
+ "dispatching": [],
247
248
  "error": None,
248
249
  "runLeafMessageId": None,
249
250
  }
@@ -437,6 +438,7 @@ class RunManager:
437
438
  if self._task is not None:
438
439
  raise RuntimeError("a run is already in flight")
439
440
  message_meta = {m["id"]: m.get("meta") for m in messages}
441
+ dispatching = list(messages)
440
442
  messages = [
441
443
  {k: v for k, v in message.items() if k != "meta"} for message in messages
442
444
  ]
@@ -450,6 +452,7 @@ class RunManager:
450
452
  self._run_acked = False
451
453
  entry = self._ensure_entry()
452
454
  self._leaf_confirmed = plain(entry["runLeafMessageId"])
455
+ entry["dispatching"] = dispatching
453
456
  entry["error"] = None
454
457
  if messages:
455
458
  self._dispatched_ids = tuple(m["id"] for m in messages)
@@ -569,6 +572,7 @@ class RunManager:
569
572
  if ack is not None:
570
573
  ack()
571
574
  self._dispatching = []
575
+ self._entry()["dispatching"] = []
572
576
  self._run_acked = True
573
577
  self._leaf_confirmed = plain(self._entry()["runLeafMessageId"])
574
578
  self._settle_sends()
@@ -577,9 +581,10 @@ class RunManager:
577
581
  def _revert_dispatching(self) -> None:
578
582
  self._settle_sends()
579
583
  taken, self._dispatching = self._dispatching, []
584
+ entry = self._ensure_entry()
585
+ entry["dispatching"] = []
580
586
  if not taken:
581
587
  return
582
- entry = self._ensure_entry()
583
588
  for lane in ("steerQueue", "queue"):
584
589
  front = [item for taken_lane, item in taken if taken_lane == lane]
585
590
  if front:
@@ -1219,6 +1224,7 @@ class RunManager:
1219
1224
  entry = manager._entry()
1220
1225
  entry["steerQueue"] = []
1221
1226
  manager._dispatching.extend(("steerQueue", item) for item in items)
1227
+ entry["dispatching"] = list(plain(entry["dispatching"])) + items
1222
1228
  if items:
1223
1229
  entry["runLeafMessageId"] = items[-1]["id"]
1224
1230
  self._ctx._message_meta.update(
@@ -0,0 +1,123 @@
1
+ """Contract: replicated dispatch limbo. ``dispatching`` on the run entry is
2
+ the current dispatch's unacked messages — queue pops, steer-lane takes, and
3
+ rewind dispatches alike. A message item lives in exactly one of the lanes,
4
+ ``dispatching``, or the thread, and every transition is single-envelope
5
+ atomic: dispatch moves it lane → ``dispatching``, the messages-ack clears
6
+ ``dispatching`` as the canonical message appears, and an unacked run end
7
+ returns lane items to the front of their lane."""
8
+
9
+ import copy
10
+
11
+ from run_helpers import Script, add, msg, queue_ids, run_host
12
+
13
+ from harness_sdk import RunManager
14
+ from statewire_helpers import apply_ops
15
+
16
+
17
+ def dispatching_ids(replica):
18
+ runs = replica["runs"]
19
+ return [item["id"] for item in runs[0]["dispatching"]] if runs else []
20
+
21
+
22
+ def places(state, message_id):
23
+ entry = (state.get("runs") or [{}])[0]
24
+ found = {
25
+ name
26
+ for name in ("queue", "steerQueue", "dispatching")
27
+ if any(item["id"] == message_id for item in entry.get(name, []))
28
+ }
29
+ if any(m["id"] == message_id for m in state.get("messages", [])):
30
+ found.add("messages")
31
+ return found
32
+
33
+
34
+ def residence(drv, message_id):
35
+ """The item's place after each envelope, deduped: exactly one place per
36
+ envelope once it exists — the one-of-three invariant."""
37
+ state: dict = {}
38
+ trail: list[str] = []
39
+ for env in copy.deepcopy(drv.envelopes):
40
+ apply_ops(state, env)
41
+ found = places(state, message_id)
42
+ assert len(found) <= 1, found
43
+ if trail:
44
+ assert found, "item vanished between envelopes"
45
+ if found and (not trail or trail[-1] != next(iter(found))):
46
+ trail.append(next(iter(found)))
47
+ return trail
48
+
49
+
50
+ async def test_idle_send_dispatches_straight_into_dispatching():
51
+ script = Script()
52
+ async with run_host(script) as (drv, host):
53
+ drv.post("run/enqueue", add("m1"))
54
+ await script.next_call()
55
+ await drv.wait_status("running")
56
+ assert dispatching_ids(drv.replica) == ["m1"]
57
+ assert queue_ids(drv.replica, "queue") == []
58
+ assert residence(drv, "m1") == ["dispatching"]
59
+
60
+
61
+ async def test_ack_clears_dispatching_with_the_message_append():
62
+ script = Script()
63
+ async with run_host(script) as (drv, host):
64
+ seq = drv.post("run/enqueue", add("m1"))
65
+ call = await script.next_call()
66
+ await drv.wait_status("running")
67
+ host.live.state["messages"] = [msg("m1")]
68
+ call.ack()
69
+ assert (await drv.res(seq))["type"] == "accepted"
70
+ assert dispatching_ids(drv.replica) == []
71
+ assert residence(drv, "m1") == ["dispatching", "messages"]
72
+ call.finish(RunManager.Complete())
73
+
74
+
75
+ async def test_unacked_run_end_returns_the_item_to_the_lane_front():
76
+ script = Script()
77
+ async with run_host(script) as (drv, host):
78
+ drv.post("run/enqueue", add("m1"))
79
+ call = await script.next_call()
80
+ await drv.side("run/enqueue", add("m2"))
81
+ await drv.wait(lambda s: queue_ids(s, "queue") == ["m2"])
82
+ await drv.side("run/stop", {"runId": await drv.run_id()})
83
+ call.finish(RunManager.Stop(dispatch_queue=False))
84
+ await drv.wait_status("stopped")
85
+ assert queue_ids(drv.replica, "queue") == ["m1", "m2"]
86
+ assert dispatching_ids(drv.replica) == []
87
+ assert residence(drv, "m1") == ["dispatching", "queue"]
88
+
89
+
90
+ async def test_steering_take_moves_the_lane_into_dispatching():
91
+ script = Script()
92
+ async with run_host(script) as (drv, host):
93
+ drv.post("run/enqueue", add("m1"))
94
+ call = await script.next_call()
95
+ host.live.state["messages"] = [msg("m1")]
96
+ call.ack()
97
+ await drv.command("run/steer", add("s1"), terminal=False)
98
+ call.ctx.steering.take()
99
+ await drv.wait(lambda s: dispatching_ids(s) == ["s1"])
100
+ assert queue_ids(drv.replica, "steerQueue") == []
101
+ assert residence(drv, "s1") == ["steerQueue", "dispatching"]
102
+ host.live.state["messages"] = [msg("m1"), msg("s1")]
103
+ call.ack()
104
+ await drv.wait(lambda s: dispatching_ids(s) == [])
105
+ assert residence(drv, "s1") == ["steerQueue", "dispatching", "messages"]
106
+ call.finish(RunManager.Complete())
107
+
108
+
109
+ async def test_edit_replacement_rides_dispatching_until_ack():
110
+ script = Script()
111
+ script.thread["u1"] = {"parentId": None, "role": "user", "isLeaf": True}
112
+ async with run_host(script, capabilities=("rewind",)) as (drv, host):
113
+ seq = drv.post("run/edit", {"sourceId": "u1", "message": msg("u2")})
114
+ rerun = await script.next_call()
115
+ assert rerun.ctx.trigger == "message-edit"
116
+ await drv.wait_status("running")
117
+ assert dispatching_ids(drv.replica) == ["u2"]
118
+ host.live.state["messages"] = [msg("u2")]
119
+ rerun.ack()
120
+ assert (await drv.res(seq))["type"] == "accepted"
121
+ assert dispatching_ids(drv.replica) == []
122
+ assert residence(drv, "u2") == ["dispatching", "messages"]
123
+ rerun.finish(RunManager.Complete())