harness-sdk-python 0.24.0__tar.gz → 0.25.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.24.0 → harness_sdk_python-0.25.1}/PKG-INFO +2 -2
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/pyproject.toml +2 -2
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/src/harness_sdk/__init__.py +2 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/src/harness_sdk/checkpoints.py +40 -12
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/run_helpers.py +4 -4
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_checkpoints.py +66 -33
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/.gitignore +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/README.md +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/examples/__init__.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/examples/runs_app.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/src/harness_sdk/linear_thread.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/src/harness_sdk/run_manager.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_admit_steer.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_applied_visibility.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_batches.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_branch_anchor.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_dispatching.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_durability.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_edit_dispatched.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_edit_reload.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_enqueue.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_input_required.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_linear_thread.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_meta.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_outcomes.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_placement.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_prepare_hooks.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_restore.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_rewind_during_run.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_run_crash_sweep.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_settle.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_steer.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_stop_continue.py +0 -0
- {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_stopping_window.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: harness-sdk-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.25.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.12
|
|
8
8
|
Requires-Dist: pinned<0.15,>=0.14.0
|
|
9
|
-
Requires-Dist: statewire<0.
|
|
9
|
+
Requires-Dist: statewire<0.13,>=0.12.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'
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "harness-sdk-python"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.25.1"
|
|
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.
|
|
8
|
+
dependencies = ["statewire>=0.12.0,<0.13", "pinned>=0.14.0,<0.15"]
|
|
9
9
|
|
|
10
10
|
[project.optional-dependencies]
|
|
11
11
|
postgres = ["langgraph-checkpoint-postgres>=2.0.0"]
|
|
@@ -8,6 +8,7 @@ from .checkpoints import (
|
|
|
8
8
|
Snapshot,
|
|
9
9
|
checkpoint_thread,
|
|
10
10
|
langgraph_checkpoints,
|
|
11
|
+
langgraph_snapshot,
|
|
11
12
|
langgraph_switch_values,
|
|
12
13
|
)
|
|
13
14
|
from .linear_thread import linear_thread
|
|
@@ -25,6 +26,7 @@ __all__ = [
|
|
|
25
26
|
"Snapshot",
|
|
26
27
|
"checkpoint_thread",
|
|
27
28
|
"langgraph_checkpoints",
|
|
29
|
+
"langgraph_snapshot",
|
|
28
30
|
"langgraph_switch_values",
|
|
29
31
|
"linear_thread",
|
|
30
32
|
"run_manager",
|
|
@@ -295,15 +295,31 @@ def _message_dict(message: Any) -> dict[str, Any]:
|
|
|
295
295
|
return message
|
|
296
296
|
|
|
297
297
|
|
|
298
|
+
def _values_snapshot(
|
|
299
|
+
checkpoint_id: str,
|
|
300
|
+
values: dict[str, Any],
|
|
301
|
+
messages_key: str,
|
|
302
|
+
lineage_key: str,
|
|
303
|
+
) -> Snapshot:
|
|
304
|
+
lineage = values.get(lineage_key)
|
|
305
|
+
if lineage is not None and not isinstance(lineage, dict):
|
|
306
|
+
raise ValueError(f"lineage channel must be an object, got {lineage!r}")
|
|
307
|
+
return Snapshot(
|
|
308
|
+
checkpoint_id=checkpoint_id,
|
|
309
|
+
messages=[_message_dict(m) for m in values.get(messages_key, [])],
|
|
310
|
+
lineage=lineage,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
298
314
|
def langgraph_checkpoints(
|
|
299
|
-
|
|
315
|
+
graph: Any,
|
|
300
316
|
*,
|
|
301
317
|
thread_id: str,
|
|
302
318
|
checkpoint_ns: str = "",
|
|
303
319
|
messages_key: str = "messages",
|
|
304
320
|
lineage_key: str = "lineage",
|
|
305
321
|
) -> CheckpointStore:
|
|
306
|
-
"""A CheckpointStore over a langgraph
|
|
322
|
+
"""A CheckpointStore over a compiled langgraph graph's ``aget_state``."""
|
|
307
323
|
|
|
308
324
|
class _Store:
|
|
309
325
|
async def get(self, checkpoint_id: str | None) -> Snapshot | None:
|
|
@@ -313,22 +329,34 @@ def langgraph_checkpoints(
|
|
|
313
329
|
}
|
|
314
330
|
if checkpoint_id is not None:
|
|
315
331
|
configurable["checkpoint_id"] = checkpoint_id
|
|
316
|
-
|
|
317
|
-
if
|
|
332
|
+
state = await graph.aget_state({"configurable": configurable})
|
|
333
|
+
if state.metadata is None:
|
|
318
334
|
return None
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
checkpoint_id=saved.checkpoint["id"],
|
|
325
|
-
messages=[_message_dict(m) for m in values.get(messages_key, [])],
|
|
326
|
-
lineage=lineage,
|
|
335
|
+
return _values_snapshot(
|
|
336
|
+
state.config["configurable"]["checkpoint_id"],
|
|
337
|
+
state.values,
|
|
338
|
+
messages_key,
|
|
339
|
+
lineage_key,
|
|
327
340
|
)
|
|
328
341
|
|
|
329
342
|
return _Store()
|
|
330
343
|
|
|
331
344
|
|
|
345
|
+
def langgraph_snapshot(
|
|
346
|
+
payload: dict[str, Any],
|
|
347
|
+
*,
|
|
348
|
+
messages_key: str = "messages",
|
|
349
|
+
lineage_key: str = "lineage",
|
|
350
|
+
) -> Snapshot:
|
|
351
|
+
"""A Snapshot from one ``stream_mode="checkpoints"`` event payload, for ingesting a run's own output."""
|
|
352
|
+
return _values_snapshot(
|
|
353
|
+
payload["config"]["configurable"]["checkpoint_id"],
|
|
354
|
+
payload["values"],
|
|
355
|
+
messages_key,
|
|
356
|
+
lineage_key,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
332
360
|
def langgraph_switch_values(
|
|
333
361
|
transcript: list[dict[str, Any]],
|
|
334
362
|
lineage: dict[str, Any],
|
|
@@ -189,11 +189,11 @@ class RunDriver:
|
|
|
189
189
|
async def side(self, method: str, params: Any = None, *, client_id: str = "c2"):
|
|
190
190
|
"""A second client's command: its own seq stream, its verdicts riding
|
|
191
191
|
the other stream."""
|
|
192
|
-
await attach(self._app, client_id=client_id)
|
|
193
192
|
seq = self._side_seqs[client_id] = self._side_seqs.get(client_id, 0) + 1
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
async with attach(self._app, client_id=client_id):
|
|
194
|
+
await self._submit(
|
|
195
|
+
{"cmd": [_statement(method, params, seq)]}, client_id=client_id
|
|
196
|
+
)
|
|
197
197
|
|
|
198
198
|
async def pump(self, timeout: float = 5) -> None:
|
|
199
199
|
env = await self._stream.next_event(timeout=timeout)
|
|
@@ -506,57 +506,90 @@ async def test_locate_loads_the_own_branch_tip_across_forks():
|
|
|
506
506
|
# ─── langgraph adapter ──────────────────────────────────────
|
|
507
507
|
|
|
508
508
|
|
|
509
|
-
|
|
510
|
-
from langgraph.checkpoint.
|
|
509
|
+
def _graph(state_type):
|
|
510
|
+
from langgraph.checkpoint.memory import InMemorySaver
|
|
511
|
+
from langgraph.graph import START, StateGraph
|
|
512
|
+
|
|
513
|
+
builder = StateGraph(state_type)
|
|
514
|
+
builder.add_node("node", lambda state: {})
|
|
515
|
+
builder.add_edge(START, "node")
|
|
516
|
+
return builder.compile(checkpointer=InMemorySaver())
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _messages_graph():
|
|
520
|
+
from typing import Annotated, Any, TypedDict
|
|
521
|
+
|
|
522
|
+
from langgraph.graph.message import add_messages
|
|
523
|
+
|
|
524
|
+
class State(TypedDict):
|
|
525
|
+
messages: Annotated[list, add_messages]
|
|
526
|
+
lineage: dict[str, Any] | None
|
|
527
|
+
|
|
528
|
+
return _graph(State)
|
|
511
529
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
checkpoint["id"] = checkpoint_id
|
|
515
|
-
checkpoint["channel_values"] = channel_values
|
|
516
|
-
checkpoint["channel_versions"] = versions
|
|
517
|
-
config = {"configurable": {"thread_id": THREAD, "checkpoint_ns": ""}}
|
|
518
|
-
await saver.aput(config, checkpoint, {"source": "loop", "step": 1}, versions)
|
|
530
|
+
|
|
531
|
+
CONFIG = {"configurable": {"thread_id": THREAD}}
|
|
519
532
|
|
|
520
533
|
|
|
521
534
|
async def test_langgraph_adapter_reads_latest_and_by_id():
|
|
522
|
-
|
|
523
|
-
from
|
|
535
|
+
pytest.importorskip("langgraph.graph")
|
|
536
|
+
from langchain_core.messages import AIMessage, HumanMessage
|
|
524
537
|
|
|
525
|
-
|
|
538
|
+
graph = _messages_graph()
|
|
539
|
+
await graph.ainvoke({"messages": [HumanMessage(id="u1", content="hi")]}, CONFIG)
|
|
540
|
+
store = langgraph_checkpoints(graph, thread_id=THREAD)
|
|
541
|
+
first = await store.get(None)
|
|
542
|
+
assert [m["id"] for m in first.messages] == ["u1"]
|
|
543
|
+
assert first.lineage is None
|
|
526
544
|
lineage = {"branch_ids": [0], "parent_checkpoint_id": None}
|
|
527
|
-
await
|
|
528
|
-
|
|
529
|
-
saver,
|
|
530
|
-
"2",
|
|
531
|
-
{
|
|
532
|
-
"messages": [HumanMessage(id="u1", content="hi"), msg("a1", "ai")],
|
|
533
|
-
"lineage": lineage,
|
|
534
|
-
},
|
|
545
|
+
await graph.ainvoke(
|
|
546
|
+
{"messages": [AIMessage(id="a1", content="yo")], "lineage": lineage}, CONFIG
|
|
535
547
|
)
|
|
536
|
-
store = langgraph_checkpoints(saver, thread_id=THREAD)
|
|
537
548
|
latest = await store.get(None)
|
|
538
|
-
assert latest.checkpoint_id
|
|
549
|
+
assert latest.checkpoint_id != first.checkpoint_id
|
|
539
550
|
assert [m["id"] for m in latest.messages] == ["u1", "a1"]
|
|
540
551
|
assert latest.lineage == lineage
|
|
541
|
-
|
|
542
|
-
assert
|
|
543
|
-
assert
|
|
544
|
-
assert first.lineage is None
|
|
545
|
-
other = langgraph_checkpoints(saver, thread_id="absent")
|
|
546
|
-
assert await other.get(None) is None
|
|
552
|
+
assert await store.get(first.checkpoint_id) == first
|
|
553
|
+
assert await store.get("absent") is None
|
|
554
|
+
assert await langgraph_checkpoints(graph, thread_id="other").get(None) is None
|
|
547
555
|
|
|
548
556
|
|
|
549
557
|
async def test_langgraph_adapter_rejects_a_message_without_an_id():
|
|
550
|
-
|
|
551
|
-
from
|
|
558
|
+
pytest.importorskip("langgraph.graph")
|
|
559
|
+
from typing import TypedDict
|
|
552
560
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
561
|
+
class State(TypedDict):
|
|
562
|
+
messages: list
|
|
563
|
+
|
|
564
|
+
graph = _graph(State)
|
|
565
|
+
await graph.ainvoke({"messages": [{"type": "human", "content": "no id"}]}, CONFIG)
|
|
566
|
+
store = langgraph_checkpoints(graph, thread_id=THREAD)
|
|
556
567
|
with pytest.raises(ValueError, match="without an id"):
|
|
557
568
|
await store.get(None)
|
|
558
569
|
|
|
559
570
|
|
|
571
|
+
async def test_langgraph_snapshot_ingests_the_run_output():
|
|
572
|
+
pytest.importorskip("langgraph.graph")
|
|
573
|
+
from langchain_core.messages import HumanMessage
|
|
574
|
+
|
|
575
|
+
from harness_sdk import langgraph_snapshot
|
|
576
|
+
|
|
577
|
+
graph = _messages_graph()
|
|
578
|
+
checkpoints = [
|
|
579
|
+
event
|
|
580
|
+
async for namespace, mode, event in graph.astream(
|
|
581
|
+
{"messages": [HumanMessage(id="u1", content="hi")]},
|
|
582
|
+
CONFIG,
|
|
583
|
+
stream_mode=["checkpoints"],
|
|
584
|
+
subgraphs=True,
|
|
585
|
+
)
|
|
586
|
+
if namespace == ()
|
|
587
|
+
]
|
|
588
|
+
snapshot = langgraph_snapshot(checkpoints[-1])
|
|
589
|
+
store = langgraph_checkpoints(graph, thread_id=THREAD)
|
|
590
|
+
assert snapshot == await store.get(None)
|
|
591
|
+
|
|
592
|
+
|
|
560
593
|
async def test_langgraph_switch_values_replaces_the_messages_channel():
|
|
561
594
|
pytest.importorskip("langgraph.graph")
|
|
562
595
|
from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage
|
|
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
|