harness-sdk-python 0.32.2__tar.gz → 0.33.0__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 (47) hide show
  1. harness_sdk_python-0.33.0/PKG-INFO +62 -0
  2. harness_sdk_python-0.33.0/README.md +43 -0
  3. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/examples/runs_app.py +1 -1
  4. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/pyproject.toml +2 -2
  5. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/src/harness_sdk/__init__.py +19 -1
  6. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/src/harness_sdk/checkpoints.py +83 -18
  7. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/src/harness_sdk/deepagents.py +145 -75
  8. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/src/harness_sdk/run_manager.py +160 -51
  9. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/src/harness_sdk/voice.py +83 -12
  10. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/run_helpers.py +29 -17
  11. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_admit_steer.py +4 -2
  12. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_checkpoints.py +67 -0
  13. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_deepagents_messages.py +128 -1
  14. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_fenced_postgres.py +1 -0
  15. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_input_required.py +66 -0
  16. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_mint_concurrency.py +6 -3
  17. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_outcomes.py +33 -1
  18. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_run_crash_sweep.py +39 -18
  19. harness_sdk_python-0.33.0/tests/test_run_ownership.py +261 -0
  20. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_settle.py +7 -3
  21. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_steer.py +38 -1
  22. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_stop_continue.py +3 -1
  23. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_voice.py +162 -2
  24. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_voice_delegation.py +107 -4
  25. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/voice_helpers.py +9 -1
  26. harness_sdk_python-0.32.2/PKG-INFO +0 -66
  27. harness_sdk_python-0.32.2/README.md +0 -47
  28. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/.gitignore +0 -0
  29. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/examples/__init__.py +0 -0
  30. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/src/harness_sdk/fenced_postgres.py +0 -0
  31. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/src/harness_sdk/linear_thread.py +0 -0
  32. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_applied_visibility.py +0 -0
  33. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_batches.py +0 -0
  34. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_branch_anchor.py +0 -0
  35. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_dispatching.py +0 -0
  36. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_durability.py +0 -0
  37. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_edit_dispatched.py +0 -0
  38. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_edit_reload.py +0 -0
  39. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_enqueue.py +0 -0
  40. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_linear_thread.py +0 -0
  41. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_meta.py +0 -0
  42. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_placement.py +0 -0
  43. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_prepare_hooks.py +0 -0
  44. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_restore.py +0 -0
  45. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_rewind_during_run.py +0 -0
  46. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_stopping_window.py +0 -0
  47. {harness_sdk_python-0.32.2 → harness_sdk_python-0.33.0}/tests/test_voice_frontend_tools.py +0 -0
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.5
2
+ Name: harness-sdk-python
3
+ Version: 0.33.0
4
+ Summary: RunManager: the harness-sdk runs subsystem for Python Statewire hosts
5
+ Project-URL: Repository, https://github.com/assistant-ui/harness-sdk
6
+ License-Expression: MIT
7
+ Requires-Python: <4.0,>=3.12
8
+ Requires-Dist: pinned<0.16,>=0.15.0
9
+ Requires-Dist: statewire<0.14,>=0.13.0
10
+ Provides-Extra: deepagents
11
+ Requires-Dist: deepagents>=0.6.12; extra == 'deepagents'
12
+ Requires-Dist: langchain-core>=0.3; extra == 'deepagents'
13
+ Requires-Dist: langchain-openai>=0.3; extra == 'deepagents'
14
+ Requires-Dist: langgraph>=1.0.0; extra == 'deepagents'
15
+ Requires-Dist: uvicorn[standard]>=0.30.0; extra == 'deepagents'
16
+ Provides-Extra: postgres
17
+ Requires-Dist: langgraph-checkpoint-postgres>=2.0.0; extra == 'postgres'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # harness-sdk-python
21
+
22
+ The runs subsystem for Python Statewire hosts: `@run_manager` turns one async method into the run executor and mounts the `run/*` command surface (enqueue, steer, dequeue, edit, reload, stop, continue, input), each run in its own tray. Part of [harness-sdk](https://github.com/assistant-ui/harness-sdk), with `statewire` and `pinned`.
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ uv add harness-sdk-python
28
+ ```
29
+
30
+ Extras: `uv add "harness-sdk-python[postgres]"`, `uv add "harness-sdk-python[deepagents]"`.
31
+
32
+ ## Use
33
+
34
+ ```python
35
+ from statewire import Statewire, command
36
+
37
+ from harness_sdk import RunManager, run_manager
38
+
39
+
40
+ class RunsHost(Statewire):
41
+ async def lifespan(self):
42
+ self.state = {"messages": []}
43
+ yield
44
+
45
+ @run_manager()
46
+ async def run(self, ctx: RunManager.RunContext):
47
+ for message in ctx.messages:
48
+ self.state["messages"].append({"id": message["id"]})
49
+ ctx.applied()
50
+ return RunManager.Complete()
51
+
52
+ @command("run/enqueue")
53
+ async def run_enqueue(self, params, *, ctx):
54
+ return await (await self.run.get()).enqueue(params, applied=ctx.applied)
55
+ ```
56
+
57
+ ## Docs
58
+
59
+ - Get started: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/get-started/chat-app/page.mdx
60
+ - Guide: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/guides/harness/host/page.mdx
61
+ - Reference: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/reference/harness-sdk-python/page.mdx
62
+ - Spec: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/spec/runs-protocol/page.mdx
@@ -0,0 +1,43 @@
1
+ # harness-sdk-python
2
+
3
+ The runs subsystem for Python Statewire hosts: `@run_manager` turns one async method into the run executor and mounts the `run/*` command surface (enqueue, steer, dequeue, edit, reload, stop, continue, input), each run in its own tray. Part of [harness-sdk](https://github.com/assistant-ui/harness-sdk), with `statewire` and `pinned`.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ uv add harness-sdk-python
9
+ ```
10
+
11
+ Extras: `uv add "harness-sdk-python[postgres]"`, `uv add "harness-sdk-python[deepagents]"`.
12
+
13
+ ## Use
14
+
15
+ ```python
16
+ from statewire import Statewire, command
17
+
18
+ from harness_sdk import RunManager, run_manager
19
+
20
+
21
+ class RunsHost(Statewire):
22
+ async def lifespan(self):
23
+ self.state = {"messages": []}
24
+ yield
25
+
26
+ @run_manager()
27
+ async def run(self, ctx: RunManager.RunContext):
28
+ for message in ctx.messages:
29
+ self.state["messages"].append({"id": message["id"]})
30
+ ctx.applied()
31
+ return RunManager.Complete()
32
+
33
+ @command("run/enqueue")
34
+ async def run_enqueue(self, params, *, ctx):
35
+ return await (await self.run.get()).enqueue(params, applied=ctx.applied)
36
+ ```
37
+
38
+ ## Docs
39
+
40
+ - Get started: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/get-started/chat-app/page.mdx
41
+ - Guide: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/guides/harness/host/page.mdx
42
+ - Reference: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/reference/harness-sdk-python/page.mdx
43
+ - Spec: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/spec/runs-protocol/page.mdx
@@ -193,6 +193,6 @@ worker = PinnedWorker(
193
193
  advertise_url=os.environ.get("ADVERTISE_URL"),
194
194
  )
195
195
  router = PinnedHost(RunsHost, worker=worker, namespace="/threads")
196
- app = FastAPI(lifespan=router.lifespan)
196
+ app = FastAPI()
197
197
  app.include_router(worker)
198
198
  app.include_router(router)
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "harness-sdk-python"
3
- version = "0.32.2"
3
+ version = "0.33.0"
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.12.3,<0.13", "pinned>=0.14.6,<0.15"]
8
+ dependencies = ["statewire>=0.13.0,<0.14", "pinned>=0.15.0,<0.16"]
9
9
 
10
10
  [project.optional-dependencies]
11
11
  postgres = ["langgraph-checkpoint-postgres>=2.0.0"]
@@ -8,16 +8,26 @@ from .checkpoints import (
8
8
  Snapshot,
9
9
  checkpoint_thread,
10
10
  langgraph_checkpoints,
11
+ langgraph_checkpointer,
11
12
  langgraph_snapshot,
13
+ langgraph_state_snapshot,
12
14
  langgraph_switch_values,
13
15
  )
14
- from .deepagents import DeepagentsMessages
16
+ from .deepagents import (
17
+ DeepagentsMessages,
18
+ deepagents_drop_ephemeral,
19
+ deepagents_ingest,
20
+ deepagents_restamp,
21
+ deepagents_role,
22
+ deepagents_upsert_ephemeral,
23
+ )
15
24
  from .linear_thread import linear_thread
16
25
  from .run_manager import RunManager, RunManagerDescriptor, run_manager
17
26
  from .voice import (
18
27
  VoiceManager,
19
28
  VoiceManagerDescriptor,
20
29
  VoiceProvider,
30
+ VoiceTurnCommitDeferred,
21
31
  end_voice,
22
32
  start_voice,
23
33
  temp_frontend_tool_result,
@@ -39,10 +49,18 @@ __all__ = [
39
49
  "VoiceManager",
40
50
  "VoiceManagerDescriptor",
41
51
  "VoiceProvider",
52
+ "VoiceTurnCommitDeferred",
42
53
  "checkpoint_thread",
54
+ "deepagents_drop_ephemeral",
55
+ "deepagents_ingest",
56
+ "deepagents_restamp",
57
+ "deepagents_role",
58
+ "deepagents_upsert_ephemeral",
43
59
  "end_voice",
44
60
  "langgraph_checkpoints",
61
+ "langgraph_checkpointer",
45
62
  "langgraph_snapshot",
63
+ "langgraph_state_snapshot",
46
64
  "langgraph_switch_values",
47
65
  "linear_thread",
48
66
  "run_manager",
@@ -27,6 +27,7 @@ _TIP_TRIGGERS = (
27
27
  "stop-continue",
28
28
  "steer",
29
29
  )
30
+ _UNREAD: Any = object()
30
31
 
31
32
 
32
33
  @dataclass(frozen=True)
@@ -326,37 +327,66 @@ def _values_snapshot(
326
327
  lineage = values.get(lineage_key)
327
328
  if lineage is not None and not isinstance(lineage, dict):
328
329
  raise ValueError(f"lineage channel must be an object, got {lineage!r}")
330
+ # an unwritten channel is absent from the checkpoint
331
+ messages = values.get(messages_key, [])
332
+ if not isinstance(messages, list):
333
+ raise TypeError(f"{messages_key} channel must be a list, got {messages!r}")
329
334
  return Snapshot(
330
335
  checkpoint_id=checkpoint_id,
331
- messages=[_message_dict(m) for m in values.get(messages_key, [])],
336
+ messages=[_message_dict(m) for m in messages],
332
337
  lineage=lineage,
333
338
  )
334
339
 
335
340
 
336
- def langgraph_checkpoints(
337
- graph: Any,
341
+ def langgraph_state_snapshot(
342
+ state: Any,
343
+ *,
344
+ messages_key: str = "messages",
345
+ lineage_key: str = "lineage",
346
+ ) -> Snapshot | None:
347
+ """A Snapshot from a compiled graph's ``aget_state`` result; None for a thread without a checkpoint."""
348
+ if state.metadata is None:
349
+ return None
350
+ return _values_snapshot(
351
+ state.config["configurable"]["checkpoint_id"],
352
+ state.values,
353
+ messages_key,
354
+ lineage_key,
355
+ )
356
+
357
+
358
+ def _thread_config(
359
+ thread_id: str, checkpoint_ns: str, checkpoint_id: str | None
360
+ ) -> dict[str, Any]:
361
+ configurable: dict[str, Any] = {
362
+ "thread_id": thread_id,
363
+ "checkpoint_ns": checkpoint_ns,
364
+ }
365
+ if checkpoint_id is not None:
366
+ configurable["checkpoint_id"] = checkpoint_id
367
+ return {"configurable": configurable}
368
+
369
+
370
+ def langgraph_checkpointer(
371
+ checkpointer: Any,
338
372
  *,
339
373
  thread_id: str,
340
374
  checkpoint_ns: str = "",
341
375
  messages_key: str = "messages",
342
376
  lineage_key: str = "lineage",
343
377
  ) -> CheckpointStore:
344
- """A CheckpointStore over a compiled langgraph graph's ``aget_state``."""
378
+ """A CheckpointStore over a langgraph checkpoint saver's ``aget_tuple``."""
345
379
 
346
380
  class _Store:
347
381
  async def get(self, checkpoint_id: str | None) -> Snapshot | None:
348
- configurable: dict[str, Any] = {
349
- "thread_id": thread_id,
350
- "checkpoint_ns": checkpoint_ns,
351
- }
352
- if checkpoint_id is not None:
353
- configurable["checkpoint_id"] = checkpoint_id
354
- state = await graph.aget_state({"configurable": configurable})
355
- if state.metadata is None:
382
+ saved = await checkpointer.aget_tuple(
383
+ _thread_config(thread_id, checkpoint_ns, checkpoint_id)
384
+ )
385
+ if saved is None:
356
386
  return None
357
387
  return _values_snapshot(
358
- state.config["configurable"]["checkpoint_id"],
359
- state.values,
388
+ saved.checkpoint["id"],
389
+ saved.checkpoint["channel_values"],
360
390
  messages_key,
361
391
  lineage_key,
362
392
  )
@@ -364,6 +394,29 @@ def langgraph_checkpoints(
364
394
  return _Store()
365
395
 
366
396
 
397
+ def langgraph_checkpoints(
398
+ graph: Any,
399
+ *,
400
+ thread_id: str,
401
+ checkpoint_ns: str = "",
402
+ messages_key: str = "messages",
403
+ lineage_key: str = "lineage",
404
+ ) -> CheckpointStore:
405
+ """A CheckpointStore over a compiled langgraph graph's ``aget_state``."""
406
+
407
+ class _Store:
408
+ async def get(self, checkpoint_id: str | None) -> Snapshot | None:
409
+ return langgraph_state_snapshot(
410
+ await graph.aget_state(
411
+ _thread_config(thread_id, checkpoint_ns, checkpoint_id)
412
+ ),
413
+ messages_key=messages_key,
414
+ lineage_key=lineage_key,
415
+ )
416
+
417
+ return _Store()
418
+
419
+
367
420
  def langgraph_snapshot(
368
421
  payload: dict[str, Any],
369
422
  *,
@@ -415,6 +468,10 @@ class CheckpointThread:
415
468
  _path: list[int] = field(default_factory=lambda: [0])
416
469
  _pending_fork: dict[str, Any] | None = None
417
470
 
471
+ def abandon_plan(self) -> None:
472
+ """Discard a planned fork whose run will not be ingested."""
473
+ self._pending_fork = None
474
+
418
475
  def _projection(self):
419
476
  return linear_thread(messages=lambda: self.transcript, role=self._role)
420
477
 
@@ -463,6 +520,8 @@ class CheckpointThread:
463
520
 
464
521
  async def plan(self, trigger: str, *, rollback_to: str | None = None) -> Plan:
465
522
  """The resumption for ``trigger``; a fork trigger names its last surviving message via ``rollback_to``."""
523
+ # an abandoned plan's fork must not leak into the next turn's ingest
524
+ self.abandon_plan()
466
525
  if trigger in _TIP_TRIGGERS:
467
526
  if rollback_to is not None:
468
527
  raise ValueError(f"rollback_to is invalid for trigger {trigger!r}")
@@ -657,15 +716,21 @@ async def checkpoint_thread(
657
716
  thread_id: str,
658
717
  checkpoint_ns: str = "",
659
718
  role: Callable[[dict[str, Any]], str],
719
+ snapshot: Snapshot | None = _UNREAD,
660
720
  ) -> CheckpointThread:
661
- """Boot a CheckpointThread: the thread row and the latest snapshot load concurrently."""
721
+ """Boot a CheckpointThread: the thread row and the latest snapshot load concurrently; ``snapshot`` hands in a latest snapshot already read (``None`` for a thread without one) and skips the store read."""
662
722
  if not isinstance(thread_id, str) or not thread_id:
663
723
  raise ValueError(f"thread_id must be a non-empty string, got {thread_id!r}")
664
724
  if not callable(role):
665
725
  raise TypeError("role must be callable")
666
- branches, snapshot = await asyncio.gather(
667
- index.read_thread(thread_id, checkpoint_ns), store.get(None)
668
- )
726
+ if snapshot is _UNREAD:
727
+ branches, snapshot = await asyncio.gather(
728
+ index.read_thread(thread_id, checkpoint_ns), store.get(None)
729
+ )
730
+ else:
731
+ if snapshot is not None and not isinstance(snapshot, Snapshot):
732
+ raise TypeError(f"snapshot must be a Snapshot or None, got {snapshot!r}")
733
+ branches = await index.read_thread(thread_id, checkpoint_ns)
669
734
  thread = CheckpointThread(
670
735
  transcript=[],
671
736
  branches=branches,