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.
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/.gitignore +1 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/PKG-INFO +6 -1
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/pyproject.toml +7 -1
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/src/harness_sdk/run_manager.py +7 -1
- harness_sdk_python-0.7.1/tests/test_dispatching.py +123 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/README.md +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/src/harness_sdk/linear_thread.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/run_helpers.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_ack_visibility.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_batches.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_branch_anchor.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_edit_dispatched.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_edit_reload.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_enqueue.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_facade.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_input_required.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_linear_thread.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_meta.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_outcomes.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_placement.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_prepare_hooks.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_rewind_during_run.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_run_leaf.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_settle.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_steer.py +0 -0
- {harness_sdk_python-0.7.0 → harness_sdk_python-0.7.1}/tests/test_stop_continue.py +0 -0
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: harness-sdk-python
|
|
3
|
-
Version: 0.7.
|
|
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.
|
|
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())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|