harness-sdk-python 0.39.0__tar.gz → 0.39.2__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.39.0 → harness_sdk_python-0.39.2}/PKG-INFO +6 -3
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/README.md +4 -1
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/examples/runs_app.py +8 -2
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/pyproject.toml +2 -2
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/deepagents_host.py +2 -2
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/protocol.py +1 -1
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/run_manager.py +24 -3
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/run_helpers.py +37 -4
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_branch_anchor.py +91 -8
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_deepagents_thread.py +27 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_threads.py +3 -3
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/voice_helpers.py +2 -2
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/.gitignore +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/examples/__init__.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/checkpoints.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/deepagents.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/interest.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/langgraph.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/linear_thread.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/messages.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/parts.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/providers/__init__.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/providers/gpt_live.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/threads.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/voice.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/voice_thread.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/wire.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_admit_steer.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_applied_visibility.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_batches.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_checkpoints.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_deepagents_approval.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_deepagents_files.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_deepagents_outcome.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_deepagents_voice.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_dispatching.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_durability.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_edit_dispatched.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_edit_reload.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_enqueue.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_gpt_live.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_imports.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_input_required.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_interest.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_langgraph.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_linear_thread.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_meta.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_mint_concurrency.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_outcomes.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_parts.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_placement.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_prepare_hooks.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_restore.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_rewind_during_run.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_run_admission.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_run_crash_sweep.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_run_eviction.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_run_id_bounds.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_run_ownership.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_run_sync.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_settle.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_steer.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_stop_continue.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_stopping_window.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_undo.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_voice.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_voice_delegation.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_voice_events.py +0 -0
- {harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/tests/test_wire.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: harness-sdk-python
|
|
3
|
-
Version: 0.39.
|
|
3
|
+
Version: 0.39.2
|
|
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.17,>=0.16.0
|
|
9
|
-
Requires-Dist: statewire<0.
|
|
9
|
+
Requires-Dist: statewire<0.24,>=0.21.2
|
|
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'
|
|
@@ -57,7 +57,10 @@ class RunsHost(Statewire):
|
|
|
57
57
|
@command("run/enqueue")
|
|
58
58
|
async def run_enqueue(self, params, *, ctx, auth_result):
|
|
59
59
|
return await self.run.enqueue(
|
|
60
|
-
params,
|
|
60
|
+
params,
|
|
61
|
+
applied=ctx.applied,
|
|
62
|
+
auth_result=auth_result,
|
|
63
|
+
version=ctx.version,
|
|
61
64
|
)
|
|
62
65
|
```
|
|
63
66
|
|
|
@@ -35,7 +35,10 @@ class RunsHost(Statewire):
|
|
|
35
35
|
@command("run/enqueue")
|
|
36
36
|
async def run_enqueue(self, params, *, ctx, auth_result):
|
|
37
37
|
return await self.run.enqueue(
|
|
38
|
-
params,
|
|
38
|
+
params,
|
|
39
|
+
applied=ctx.applied,
|
|
40
|
+
auth_result=auth_result,
|
|
41
|
+
version=ctx.version,
|
|
39
42
|
)
|
|
40
43
|
```
|
|
41
44
|
|
|
@@ -158,13 +158,19 @@ class RunsHost(Statewire):
|
|
|
158
158
|
@command("run/enqueue")
|
|
159
159
|
async def run_enqueue(self, params, *, ctx, auth_result):
|
|
160
160
|
return await self.run.enqueue(
|
|
161
|
-
params,
|
|
161
|
+
params,
|
|
162
|
+
applied=ctx.applied,
|
|
163
|
+
auth_result=auth_result,
|
|
164
|
+
version=ctx.version,
|
|
162
165
|
)
|
|
163
166
|
|
|
164
167
|
@command("run/steer")
|
|
165
168
|
async def run_steer(self, params, *, ctx, auth_result):
|
|
166
169
|
return await self.run.steer(
|
|
167
|
-
params,
|
|
170
|
+
params,
|
|
171
|
+
applied=ctx.applied,
|
|
172
|
+
auth_result=auth_result,
|
|
173
|
+
version=ctx.version,
|
|
168
174
|
)
|
|
169
175
|
|
|
170
176
|
@command("run/dequeue")
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "harness-sdk-python"
|
|
3
|
-
version = "0.39.
|
|
3
|
+
version = "0.39.2"
|
|
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.21.
|
|
8
|
+
dependencies = ["statewire>=0.21.2,<0.24", "pinned>=0.16.0,<0.17"]
|
|
9
9
|
|
|
10
10
|
[project.optional-dependencies]
|
|
11
11
|
postgres = ["langgraph-checkpoint-postgres>=2.0.0"]
|
|
@@ -189,11 +189,11 @@ class DeepagentsHost(VoiceThread, HarnessMessages, Statewire):
|
|
|
189
189
|
|
|
190
190
|
@command("run/enqueue")
|
|
191
191
|
async def run_enqueue(self, params, *, ctx, auth_result):
|
|
192
|
-
return await self.run.enqueue(params, applied=ctx.applied, auth_result=auth_result)
|
|
192
|
+
return await self.run.enqueue(params, applied=ctx.applied, auth_result=auth_result, version=ctx.version)
|
|
193
193
|
|
|
194
194
|
@command("run/steer")
|
|
195
195
|
async def run_steer(self, params, *, ctx, auth_result):
|
|
196
|
-
return await self.run.steer(params, applied=ctx.applied, auth_result=auth_result)
|
|
196
|
+
return await self.run.steer(params, applied=ctx.applied, auth_result=auth_result, version=ctx.version)
|
|
197
197
|
|
|
198
198
|
@command("run/dequeue")
|
|
199
199
|
async def run_dequeue(self, params, *, auth_result):
|
|
@@ -88,6 +88,7 @@ MAX_MESSAGE_BYTES = 1 << 20
|
|
|
88
88
|
MAX_RUN_IDS = 4096
|
|
89
89
|
|
|
90
90
|
_ABSENT: Any = object()
|
|
91
|
+
_LEAF_SEND_VERSION = "2026-11-02"
|
|
91
92
|
_PARKED: Any = object()
|
|
92
93
|
|
|
93
94
|
_CAPABILITIES = frozenset(
|
|
@@ -310,6 +311,7 @@ class _Send:
|
|
|
310
311
|
thread_empty: bool = False
|
|
311
312
|
minted: bool = False
|
|
312
313
|
principal: Any = None
|
|
314
|
+
version: str | None = None
|
|
313
315
|
future: "asyncio.Future[Any]" = field(default_factory=_future)
|
|
314
316
|
|
|
315
317
|
|
|
@@ -1252,7 +1254,12 @@ class RunManager(Tray):
|
|
|
1252
1254
|
if len(self._batch) + len(steer) >= self.max_queued:
|
|
1253
1255
|
raise _reject("queue-full", f"queue is full ({self.max_queued} messages)")
|
|
1254
1256
|
staged = self._stamped(e.message, e.meta)
|
|
1255
|
-
if
|
|
1257
|
+
if (
|
|
1258
|
+
not self._batch
|
|
1259
|
+
and not steer
|
|
1260
|
+
and e.anchor is not _ABSENT
|
|
1261
|
+
and not self._stale_anchor(e)
|
|
1262
|
+
):
|
|
1256
1263
|
staged["~parent"] = e.anchor
|
|
1257
1264
|
self._batch.extend(("steerQueue", item) for item in steer)
|
|
1258
1265
|
self._batch.append((e.lane, staged))
|
|
@@ -1380,6 +1387,15 @@ class RunManager(Tray):
|
|
|
1380
1387
|
if not e.anchor_meta["onActiveBranch"]:
|
|
1381
1388
|
raise _reject("wrong-anchor", f"anchor {e.anchor} is off the active branch")
|
|
1382
1389
|
|
|
1390
|
+
def _stale_anchor(self, e: _Send) -> bool:
|
|
1391
|
+
return (
|
|
1392
|
+
e.version is not None
|
|
1393
|
+
and e.version >= _LEAF_SEND_VERSION
|
|
1394
|
+
and e.anchor_meta is not None
|
|
1395
|
+
and not e.anchor_meta["isLeaf"]
|
|
1396
|
+
and e.anchor not in self.dispatchedIds.value
|
|
1397
|
+
)
|
|
1398
|
+
|
|
1383
1399
|
# ─── Queue mutations ────────────────────────────────────
|
|
1384
1400
|
|
|
1385
1401
|
def _stamped(self, message: dict[str, Any], meta: Any) -> dict[str, Any]:
|
|
@@ -2106,6 +2122,7 @@ class RunManagerNamespace(TrayMapNamespace):
|
|
|
2106
2122
|
meta: Any,
|
|
2107
2123
|
applied: Callable[[], None],
|
|
2108
2124
|
auth_result: Any,
|
|
2125
|
+
version: str | None,
|
|
2109
2126
|
) -> Any:
|
|
2110
2127
|
cls: type[RunManager] = self._spec.tray_cls # type: ignore[assignment]
|
|
2111
2128
|
if not isinstance(params, dict):
|
|
@@ -2151,6 +2168,7 @@ class RunManagerNamespace(TrayMapNamespace):
|
|
|
2151
2168
|
anchor_meta=anchor_meta,
|
|
2152
2169
|
thread_empty=thread_empty,
|
|
2153
2170
|
principal=auth_result,
|
|
2171
|
+
version=version,
|
|
2154
2172
|
)
|
|
2155
2173
|
)
|
|
2156
2174
|
message = _validated_message(cls, params["message"])
|
|
@@ -2171,6 +2189,7 @@ class RunManagerNamespace(TrayMapNamespace):
|
|
|
2171
2189
|
thread_empty=thread_empty,
|
|
2172
2190
|
minted=minted,
|
|
2173
2191
|
principal=auth_result,
|
|
2192
|
+
version=version,
|
|
2174
2193
|
)
|
|
2175
2194
|
)
|
|
2176
2195
|
|
|
@@ -2181,8 +2200,9 @@ class RunManagerNamespace(TrayMapNamespace):
|
|
|
2181
2200
|
meta: Any = None,
|
|
2182
2201
|
applied: Callable[[], None],
|
|
2183
2202
|
auth_result: Any,
|
|
2203
|
+
version: str | None = None,
|
|
2184
2204
|
) -> Any:
|
|
2185
|
-
return await self._send("queue", params, meta, applied, auth_result)
|
|
2205
|
+
return await self._send("queue", params, meta, applied, auth_result, version)
|
|
2186
2206
|
|
|
2187
2207
|
async def steer(
|
|
2188
2208
|
self,
|
|
@@ -2191,8 +2211,9 @@ class RunManagerNamespace(TrayMapNamespace):
|
|
|
2191
2211
|
meta: Any = None,
|
|
2192
2212
|
applied: Callable[[], None],
|
|
2193
2213
|
auth_result: Any,
|
|
2214
|
+
version: str | None = None,
|
|
2194
2215
|
) -> Any:
|
|
2195
|
-
return await self._send("steerQueue", params, meta, applied, auth_result)
|
|
2216
|
+
return await self._send("steerQueue", params, meta, applied, auth_result, version)
|
|
2196
2217
|
|
|
2197
2218
|
async def dequeue(self, params: Any, *, auth_result: Any) -> None:
|
|
2198
2219
|
message_id = params.get("messageId") if isinstance(params, dict) else None
|
|
@@ -8,9 +8,11 @@ from uuid import uuid4
|
|
|
8
8
|
from pinned import PinnedWorker
|
|
9
9
|
from pinned.testing import MemoryStateStore
|
|
10
10
|
from statewire import Statewire, command
|
|
11
|
+
from statewire.protocol import VERSION, Offer, Protocol, VersionRange
|
|
12
|
+
from statewire.protocol import headers as protocol_headers
|
|
11
13
|
from statewire_helpers import apply_ops, attach, post_frame, statewire_client, stream_of
|
|
12
14
|
|
|
13
|
-
from harness_sdk import RunManager, run_manager
|
|
15
|
+
from harness_sdk import HARNESS_PROTOCOL, RunManager, run_manager
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
@dataclass
|
|
@@ -53,6 +55,11 @@ class Script:
|
|
|
53
55
|
assert self.calls.empty(), "unexpected start() invocation"
|
|
54
56
|
|
|
55
57
|
|
|
58
|
+
def negotiating(version: str) -> dict[str, str]:
|
|
59
|
+
"""Attach headers offering only ``version`` of the test host's protocol."""
|
|
60
|
+
return protocol_headers(VersionRange(VERSION), (Offer("default", VersionRange(version)),))
|
|
61
|
+
|
|
62
|
+
|
|
56
63
|
def _meta(params: Any) -> Any:
|
|
57
64
|
"""Test hosts stamp meta from a wire `meta` param; real hosts stamp their own."""
|
|
58
65
|
return params.pop("meta", None) if isinstance(params, dict) else None
|
|
@@ -65,8 +72,16 @@ def make_host(
|
|
|
65
72
|
prepare_message=None,
|
|
66
73
|
prepare_input=None,
|
|
67
74
|
authorize=None,
|
|
75
|
+
version=HARNESS_PROTOCOL.version,
|
|
68
76
|
):
|
|
69
77
|
class Host(Statewire):
|
|
78
|
+
protocols = (
|
|
79
|
+
Protocol(
|
|
80
|
+
"default",
|
|
81
|
+
version=HARNESS_PROTOCOL.version,
|
|
82
|
+
min_version=HARNESS_PROTOCOL.min_version,
|
|
83
|
+
),
|
|
84
|
+
)
|
|
70
85
|
live: "Host | None" = None
|
|
71
86
|
|
|
72
87
|
async def lifespan(self):
|
|
@@ -89,13 +104,21 @@ def make_host(
|
|
|
89
104
|
@command("run/enqueue")
|
|
90
105
|
async def run_enqueue(self, params, *, ctx, auth_result):
|
|
91
106
|
return await self.runs.enqueue(
|
|
92
|
-
params,
|
|
107
|
+
params,
|
|
108
|
+
meta=_meta(params),
|
|
109
|
+
applied=ctx.applied,
|
|
110
|
+
auth_result=auth_result,
|
|
111
|
+
version=version,
|
|
93
112
|
)
|
|
94
113
|
|
|
95
114
|
@command("run/steer")
|
|
96
115
|
async def run_steer(self, params, *, ctx, auth_result):
|
|
97
116
|
return await self.runs.steer(
|
|
98
|
-
params,
|
|
117
|
+
params,
|
|
118
|
+
meta=_meta(params),
|
|
119
|
+
applied=ctx.applied,
|
|
120
|
+
auth_result=auth_result,
|
|
121
|
+
version=version,
|
|
99
122
|
)
|
|
100
123
|
|
|
101
124
|
@command("run/dequeue")
|
|
@@ -315,7 +338,9 @@ async def run_host(
|
|
|
315
338
|
store=None,
|
|
316
339
|
authorize=None,
|
|
317
340
|
headers=None,
|
|
341
|
+
version=HARNESS_PROTOCOL.version,
|
|
318
342
|
):
|
|
343
|
+
"""``version`` is the negotiated protocol date the host passes to sends; None passes none."""
|
|
319
344
|
host_cls = make_host(
|
|
320
345
|
script,
|
|
321
346
|
capabilities=capabilities,
|
|
@@ -323,6 +348,7 @@ async def run_host(
|
|
|
323
348
|
prepare_message=prepare_message,
|
|
324
349
|
prepare_input=prepare_input,
|
|
325
350
|
authorize=authorize,
|
|
351
|
+
version=version,
|
|
326
352
|
)
|
|
327
353
|
worker = PinnedWorker(
|
|
328
354
|
state_store=store if store is not None else MemoryStateStore()
|
|
@@ -330,7 +356,14 @@ async def run_host(
|
|
|
330
356
|
async with statewire_client(host_cls, worker=worker) as (app, client):
|
|
331
357
|
# Fresh client id per session, like a production client.
|
|
332
358
|
client_id = f"c-{uuid4().hex[:8]}"
|
|
333
|
-
async with stream_of(
|
|
359
|
+
async with stream_of(
|
|
360
|
+
app,
|
|
361
|
+
client_id=client_id,
|
|
362
|
+
headers={
|
|
363
|
+
**negotiating(version or HARNESS_PROTOCOL.min_version),
|
|
364
|
+
**(headers or {}),
|
|
365
|
+
},
|
|
366
|
+
) as stream:
|
|
334
367
|
snapshot = await stream.next_event()
|
|
335
368
|
driver = RunDriver(client, stream, app, client_id=client_id)
|
|
336
369
|
driver.envelopes.append(snapshot)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import pytest
|
|
1
2
|
from run_helpers import (
|
|
2
3
|
Script,
|
|
3
4
|
add,
|
|
@@ -14,6 +15,19 @@ from run_helpers import (
|
|
|
14
15
|
|
|
15
16
|
from harness_sdk import RunManager
|
|
16
17
|
|
|
18
|
+
BRANCHING = "2026-09-30"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def two_turns():
|
|
22
|
+
script = Script()
|
|
23
|
+
script.thread.update(
|
|
24
|
+
{
|
|
25
|
+
"u1": {"parentId": None, "role": "user", "isLeaf": False, "onActiveBranch": True},
|
|
26
|
+
"a1": {"parentId": "u1", "role": "assistant", "isLeaf": True, "onActiveBranch": True},
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
return script
|
|
30
|
+
|
|
17
31
|
|
|
18
32
|
async def test_add_requires_a_run_id():
|
|
19
33
|
script = Script()
|
|
@@ -208,15 +222,28 @@ async def test_post_reload_sends_anchor_on_the_reload_targets_parent():
|
|
|
208
222
|
await drv.wait_status("running")
|
|
209
223
|
|
|
210
224
|
|
|
211
|
-
async def
|
|
212
|
-
script =
|
|
213
|
-
script.thread.update(
|
|
214
|
-
{
|
|
215
|
-
"u1": {"parentId": None, "role": "user", "isLeaf": False, "onActiveBranch": True},
|
|
216
|
-
"a1": {"parentId": "u1", "role": "assistant", "isLeaf": True, "onActiveBranch": True},
|
|
217
|
-
}
|
|
218
|
-
)
|
|
225
|
+
async def test_a_dispatching_send_carries_its_leaf_anchor_as_the_parent():
|
|
226
|
+
script = two_turns()
|
|
219
227
|
async with run_host(script) as (drv, host):
|
|
228
|
+
drv.post("run/enqueue", add("m1", anchor="a1"))
|
|
229
|
+
call = await script.next_call()
|
|
230
|
+
assert call.ctx.messages == ({**msg("m1"), "parentId": "a1"},)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
async def test_a_stale_anchor_appends_at_the_leaf():
|
|
234
|
+
script = two_turns()
|
|
235
|
+
async with run_host(script) as (drv, host):
|
|
236
|
+
drv.post("run/steer", add("m1", anchor="u1"))
|
|
237
|
+
call = await script.next_call()
|
|
238
|
+
assert call.ctx.messages == (msg("m1"),)
|
|
239
|
+
await drv.wait(lambda s: run_of(s)["dispatching"] is not None)
|
|
240
|
+
assert drv.run["dispatching"]["messages"] == [msg("m1")]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@pytest.mark.parametrize("version", [BRANCHING, None])
|
|
244
|
+
async def test_a_stale_anchor_branches_under_2026_09_30_or_an_unknown_version(version):
|
|
245
|
+
script = two_turns()
|
|
246
|
+
async with run_host(script, version=version) as (drv, host):
|
|
220
247
|
drv.post("run/enqueue", add("m1", anchor="u1"))
|
|
221
248
|
call = await script.next_call()
|
|
222
249
|
assert call.ctx.messages == ({**msg("m1"), "parentId": "u1"},)
|
|
@@ -229,6 +256,62 @@ async def test_a_dispatching_send_carries_its_anchor_as_the_parent():
|
|
|
229
256
|
assert queued.ctx.messages == (msg("m2"),)
|
|
230
257
|
|
|
231
258
|
|
|
259
|
+
async def test_off_branch_and_unknown_anchors_reject_under_2026_09_30():
|
|
260
|
+
script = two_turns()
|
|
261
|
+
script.thread["old"] = {
|
|
262
|
+
"parentId": "u1", "role": "assistant", "isLeaf": True, "onActiveBranch": False
|
|
263
|
+
}
|
|
264
|
+
async with run_host(script, version=BRANCHING) as (drv, host):
|
|
265
|
+
assert_rejected(
|
|
266
|
+
await drv.command("run/enqueue", add("m1", anchor="old"), terminal=False),
|
|
267
|
+
"wrong-anchor",
|
|
268
|
+
)
|
|
269
|
+
assert_rejected(
|
|
270
|
+
await drv.command("run/enqueue", add("m1", anchor="zz"), terminal=False),
|
|
271
|
+
"unknown-id",
|
|
272
|
+
)
|
|
273
|
+
script.no_call()
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
async def test_a_dispatched_anchor_stays_the_parent_after_an_error():
|
|
277
|
+
script = Script()
|
|
278
|
+
async with run_host(script) as (drv, host):
|
|
279
|
+
drv.post("run/enqueue", add("m1"))
|
|
280
|
+
call = await script.next_call()
|
|
281
|
+
call.applied()
|
|
282
|
+
script.thread.update(
|
|
283
|
+
{
|
|
284
|
+
"m1": {"parentId": None, "role": "user", "isLeaf": False, "onActiveBranch": True},
|
|
285
|
+
"a1": {"parentId": "m1", "role": "assistant", "isLeaf": True, "onActiveBranch": True},
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
call.fail(RuntimeError("boom"))
|
|
289
|
+
await drv.wait_status("error")
|
|
290
|
+
drv.post("run/enqueue", add("m2", anchor="m1"))
|
|
291
|
+
retry = await script.next_call()
|
|
292
|
+
assert retry.ctx.messages == ({**msg("m2"), "parentId": "m1"},)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
async def test_edit_and_reload_anchor_at_a_non_leaf_parent():
|
|
296
|
+
script = two_turns()
|
|
297
|
+
script.thread["a0"] = {
|
|
298
|
+
"parentId": None, "role": "assistant", "isLeaf": False, "onActiveBranch": True
|
|
299
|
+
}
|
|
300
|
+
script.thread["u1"] = {**script.thread["u1"], "parentId": "a0"}
|
|
301
|
+
async with run_host(script, capabilities=("rewind",)) as (drv, host):
|
|
302
|
+
drv.post("run/edit", edit_params("u1", msg("u2"), anchor="a0"))
|
|
303
|
+
edit = await script.next_call()
|
|
304
|
+
assert edit.ctx.rollback_to == "a0"
|
|
305
|
+
assert edit.ctx.messages == (msg("u2"),)
|
|
306
|
+
edit.applied()
|
|
307
|
+
edit.finish(RunManager.Complete())
|
|
308
|
+
await drv.wait_status("ready")
|
|
309
|
+
drv.post("run/reload", reload_params("a1", anchor="u1"))
|
|
310
|
+
reload = await script.next_call()
|
|
311
|
+
assert reload.ctx.trigger == "message-reload"
|
|
312
|
+
assert reload.ctx.rollback_to == "u1"
|
|
313
|
+
|
|
314
|
+
|
|
232
315
|
async def test_a_message_parent_id_is_rejected():
|
|
233
316
|
script = Script()
|
|
234
317
|
async with run_host(script) as (drv, host):
|
|
@@ -15,6 +15,7 @@ from langgraph.checkpoint.memory import InMemorySaver
|
|
|
15
15
|
from pinned import PinnedWorker
|
|
16
16
|
from pinned.testing import MemoryStateStore
|
|
17
17
|
from run_helpers import _statement, add, edit_params, msg
|
|
18
|
+
from statewire.protocol import VERSION, Offer, VersionRange, headers as protocol_headers
|
|
18
19
|
from statewire_helpers import apply_ops, post_frame, statewire_client, stream_of
|
|
19
20
|
from voice_helpers import NEGOTIATION, VoiceDriver
|
|
20
21
|
|
|
@@ -334,3 +335,29 @@ async def test_host_send_from_a_switched_branch_grows_under_its_leaf():
|
|
|
334
335
|
assert all(w[:2] == ["u1", old_leaf] for w in windows)
|
|
335
336
|
assert window(drv)[2:] == ["u3", drv.replica["threads"]["main"]["headId"]]
|
|
336
337
|
assert next(m for m in drv.mounted() if m["id"] == "u3")["parentId"] == old_leaf
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@pytest.mark.parametrize(("version", "appends"), [("2026-11-02", True), ("2026-09-30", False)])
|
|
341
|
+
async def test_host_stale_on_branch_send_appends_at_the_leaf_from_2026_11_02(version, appends):
|
|
342
|
+
"""A send anchored at a committed non-leaf message on the active branch appends at the
|
|
343
|
+
leaf for a 2026-11-02 client and branches for a 2026-09-30 one, via ``ctx.version``."""
|
|
344
|
+
|
|
345
|
+
class Fresh(Host):
|
|
346
|
+
checkpointer = InMemorySaver()
|
|
347
|
+
index = MemoryCheckpointIndex()
|
|
348
|
+
|
|
349
|
+
headers = protocol_headers(VersionRange(VERSION), (Offer("harness-sdk", VersionRange(version)),))
|
|
350
|
+
async with statewire_client(Fresh, worker=PinnedWorker(state_store=MemoryStateStore())) as (app, client):
|
|
351
|
+
async with stream_of(app, client_id="c1", headers=headers) as stream:
|
|
352
|
+
snapshot = await stream.next_event()
|
|
353
|
+
drv = VoiceDriver(client, stream, app, client_id="c1", protocol="harness-sdk")
|
|
354
|
+
apply_ops(drv.replica, copy.deepcopy(snapshot))
|
|
355
|
+
drv.fold(snapshot)
|
|
356
|
+
await drv.command("run/enqueue", add("u1", anchor=None))
|
|
357
|
+
await drv.wait(lambda s: len(window(drv)) == 2 and s["status"] == "ready")
|
|
358
|
+
leaf = window(drv)[1]
|
|
359
|
+
await drv.command("run/enqueue", add("u2", anchor="u1"))
|
|
360
|
+
await drv.wait(lambda s: "u2" in window(drv) and len(window(drv)) >= 3 and s["status"] == "ready")
|
|
361
|
+
parent = next(m for m in drv.mounted() if m["id"] == "u2")["parentId"]
|
|
362
|
+
assert parent == (leaf if appends else "u1")
|
|
363
|
+
assert window(drv)[: (3 if appends else 2)] == (["u1", leaf, "u2"] if appends else ["u1", "u2"])
|
|
@@ -9,7 +9,7 @@ from fastapi import FastAPI
|
|
|
9
9
|
from pinned import PinnedAPI, PinnedHost, PinnedWorker, route
|
|
10
10
|
from pinned.testing import MemoryStateStore
|
|
11
11
|
from run_helpers import RunDriver, Script, add, make_host
|
|
12
|
-
from statewire.protocol import Offer, VersionRange, headers as protocol_headers
|
|
12
|
+
from statewire.protocol import VERSION, Offer, VersionRange, headers as protocol_headers
|
|
13
13
|
from statewire_helpers import CLIENT_ID_HEADER, SSEStream, apply_ops, cmd_on, stream_of
|
|
14
14
|
|
|
15
15
|
from harness_sdk import RunManager
|
|
@@ -20,8 +20,8 @@ from harness_sdk.wire import HarnessMessages, first_user_text
|
|
|
20
20
|
|
|
21
21
|
pytestmark = pytest.mark.anyio
|
|
22
22
|
|
|
23
|
-
NEGOTIATION = protocol_headers(VersionRange(), (Offer("harness-sdk-threads", VersionRange("2026-09-13")),))
|
|
24
|
-
HOST_NEGOTIATION = protocol_headers(VersionRange(), (Offer("harness-sdk", VersionRange()),))
|
|
23
|
+
NEGOTIATION = protocol_headers(VersionRange(VERSION), (Offer("harness-sdk-threads", VersionRange("2026-09-13")),))
|
|
24
|
+
HOST_NEGOTIATION = protocol_headers(VersionRange(VERSION), (Offer("harness-sdk", VersionRange(VERSION)),))
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class Toucher(PinnedAPI):
|
|
@@ -10,7 +10,7 @@ from pinned.testing import MemoryStateStore
|
|
|
10
10
|
from run_helpers import RunDriver, _statement
|
|
11
11
|
from statewire import Statewire, command
|
|
12
12
|
from statewire.client import apply_ops as client_apply_ops
|
|
13
|
-
from statewire.protocol import Offer, VersionRange, headers as protocol_headers
|
|
13
|
+
from statewire.protocol import VERSION, Offer, VersionRange, headers as protocol_headers
|
|
14
14
|
from statewire_helpers import apply_ops, post_frame, statewire_client, stream_of
|
|
15
15
|
|
|
16
16
|
from harness_sdk import (
|
|
@@ -26,7 +26,7 @@ from harness_sdk import (
|
|
|
26
26
|
from harness_sdk.messages import MAIN_NS
|
|
27
27
|
from harness_sdk.protocol import HARNESS_PROTOCOL
|
|
28
28
|
|
|
29
|
-
NEGOTIATION = protocol_headers(VersionRange(), (Offer("harness-sdk", VersionRange()),))
|
|
29
|
+
NEGOTIATION = protocol_headers(VersionRange(VERSION), (Offer("harness-sdk", VersionRange(VERSION)),))
|
|
30
30
|
|
|
31
31
|
OFFER = "v=0 fake-offer"
|
|
32
32
|
ANSWER = "v=0 fake-answer"
|
|
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
|
{harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/providers/__init__.py
RENAMED
|
File without changes
|
{harness_sdk_python-0.39.0 → harness_sdk_python-0.39.2}/src/harness_sdk/providers/gpt_live.py
RENAMED
|
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
|
|
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
|