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.
Files changed (36) hide show
  1. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/PKG-INFO +2 -2
  2. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/pyproject.toml +2 -2
  3. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/src/harness_sdk/__init__.py +2 -0
  4. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/src/harness_sdk/checkpoints.py +40 -12
  5. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/run_helpers.py +4 -4
  6. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_checkpoints.py +66 -33
  7. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/.gitignore +0 -0
  8. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/README.md +0 -0
  9. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/examples/__init__.py +0 -0
  10. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/examples/runs_app.py +0 -0
  11. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/src/harness_sdk/fenced_postgres.py +0 -0
  12. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/src/harness_sdk/linear_thread.py +0 -0
  13. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/src/harness_sdk/run_manager.py +0 -0
  14. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_admit_steer.py +0 -0
  15. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_applied_visibility.py +0 -0
  16. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_batches.py +0 -0
  17. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_branch_anchor.py +0 -0
  18. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_dispatching.py +0 -0
  19. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_durability.py +0 -0
  20. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_edit_dispatched.py +0 -0
  21. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_edit_reload.py +0 -0
  22. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_enqueue.py +0 -0
  23. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_fenced_postgres.py +0 -0
  24. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_input_required.py +0 -0
  25. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_linear_thread.py +0 -0
  26. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_meta.py +0 -0
  27. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_outcomes.py +0 -0
  28. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_placement.py +0 -0
  29. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_prepare_hooks.py +0 -0
  30. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_restore.py +0 -0
  31. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_rewind_during_run.py +0 -0
  32. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_run_crash_sweep.py +0 -0
  33. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_settle.py +0 -0
  34. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_steer.py +0 -0
  35. {harness_sdk_python-0.24.0 → harness_sdk_python-0.25.1}/tests/test_stop_continue.py +0 -0
  36. {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.24.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.12,>=0.11.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.24.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.11.0,<0.12", "pinned>=0.14.0,<0.15"]
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
- saver: Any,
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 BaseCheckpointSaver's ``aget_tuple``."""
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
- saved = await saver.aget_tuple({"configurable": configurable})
317
- if saved is None:
332
+ state = await graph.aget_state({"configurable": configurable})
333
+ if state.metadata is None:
318
334
  return None
319
- values = saved.checkpoint["channel_values"]
320
- lineage = values.get(lineage_key)
321
- if lineage is not None and not isinstance(lineage, dict):
322
- raise ValueError(f"lineage channel must be an object, got {lineage!r}")
323
- return Snapshot(
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
- await self._submit(
195
- {"cmd": [_statement(method, params, seq)]}, client_id=client_id
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
- async def _saved(saver, checkpoint_id, channel_values):
510
- from langgraph.checkpoint.base import empty_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
- versions = {key: "1" for key in channel_values}
513
- checkpoint = empty_checkpoint()
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
- from langchain_core.messages import HumanMessage
523
- from langgraph.checkpoint.memory import InMemorySaver
535
+ pytest.importorskip("langgraph.graph")
536
+ from langchain_core.messages import AIMessage, HumanMessage
524
537
 
525
- saver = InMemorySaver()
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 _saved(saver, "1", {"messages": [HumanMessage(id="u1", content="hi")]})
528
- await _saved(
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 == "2"
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
- first = await store.get("1")
542
- assert first.checkpoint_id == "1"
543
- assert first.messages[0]["id"] == "u1"
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
- from langchain_core.messages import HumanMessage
551
- from langgraph.checkpoint.memory import InMemorySaver
558
+ pytest.importorskip("langgraph.graph")
559
+ from typing import TypedDict
552
560
 
553
- saver = InMemorySaver()
554
- await _saved(saver, "1", {"messages": [HumanMessage(content="no id")]})
555
- store = langgraph_checkpoints(saver, thread_id=THREAD)
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