harness-sdk-python 0.32.2__tar.gz → 0.34.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.32.2 → harness_sdk_python-0.34.0}/.gitignore +3 -0
  2. harness_sdk_python-0.34.0/PKG-INFO +64 -0
  3. harness_sdk_python-0.34.0/README.md +45 -0
  4. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/examples/runs_app.py +24 -18
  5. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/pyproject.toml +2 -2
  6. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/src/harness_sdk/__init__.py +19 -1
  7. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/src/harness_sdk/checkpoints.py +83 -18
  8. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/src/harness_sdk/deepagents.py +145 -75
  9. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/src/harness_sdk/run_manager.py +201 -105
  10. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/src/harness_sdk/voice.py +83 -12
  11. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/run_helpers.py +29 -17
  12. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_admit_steer.py +4 -2
  13. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_checkpoints.py +67 -0
  14. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_deepagents_messages.py +128 -1
  15. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_fenced_postgres.py +1 -0
  16. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_input_required.py +66 -0
  17. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_mint_concurrency.py +12 -8
  18. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_outcomes.py +33 -1
  19. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_run_crash_sweep.py +39 -18
  20. harness_sdk_python-0.34.0/tests/test_run_ownership.py +261 -0
  21. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_settle.py +7 -3
  22. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_steer.py +38 -1
  23. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_stop_continue.py +3 -1
  24. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_voice.py +162 -2
  25. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_voice_delegation.py +107 -4
  26. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/voice_helpers.py +9 -1
  27. harness_sdk_python-0.32.2/PKG-INFO +0 -66
  28. harness_sdk_python-0.32.2/README.md +0 -47
  29. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/examples/__init__.py +0 -0
  30. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/src/harness_sdk/fenced_postgres.py +0 -0
  31. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/src/harness_sdk/linear_thread.py +0 -0
  32. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_applied_visibility.py +0 -0
  33. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_batches.py +0 -0
  34. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_branch_anchor.py +0 -0
  35. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_dispatching.py +0 -0
  36. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_durability.py +0 -0
  37. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_edit_dispatched.py +0 -0
  38. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_edit_reload.py +0 -0
  39. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_enqueue.py +0 -0
  40. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_linear_thread.py +0 -0
  41. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_meta.py +0 -0
  42. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_placement.py +0 -0
  43. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_prepare_hooks.py +0 -0
  44. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_restore.py +0 -0
  45. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_rewind_during_run.py +0 -0
  46. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_stopping_window.py +0 -0
  47. {harness_sdk_python-0.32.2 → harness_sdk_python-0.34.0}/tests/test_voice_frontend_tools.py +0 -0
@@ -25,3 +25,6 @@ apps/docs/.docs
25
25
  /.deepsec/
26
26
  .vercel
27
27
  .env*
28
+
29
+ MAINTAIN_EPOCH.md
30
+ DOCS_EPOCH.md
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.5
2
+ Name: harness-sdk-python
3
+ Version: 0.34.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.15,>=0.14.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, auth_result):
54
+ return await self.run.enqueue(
55
+ params, applied=ctx.applied, auth_result=auth_result
56
+ )
57
+ ```
58
+
59
+ ## Docs
60
+
61
+ - Get started: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/get-started/chat-app/page.mdx
62
+ - Guide: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/guides/harness/host/page.mdx
63
+ - Reference: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/reference/harness-sdk-python/page.mdx
64
+ - Spec: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/spec/runs-protocol/page.mdx
@@ -0,0 +1,45 @@
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, auth_result):
35
+ return await self.run.enqueue(
36
+ params, applied=ctx.applied, auth_result=auth_result
37
+ )
38
+ ```
39
+
40
+ ## Docs
41
+
42
+ - Get started: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/get-started/chat-app/page.mdx
43
+ - Guide: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/guides/harness/host/page.mdx
44
+ - Reference: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/reference/harness-sdk-python/page.mdx
45
+ - Spec: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/docs/spec/runs-protocol/page.mdx
@@ -20,7 +20,7 @@ from typing import Any
20
20
  from fastapi import FastAPI
21
21
  from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
22
22
  from pinned import PinnedHost, PinnedWorker
23
- from pinned.testing import MemoryStateStore
23
+ from pinned.durability import MemoryStateStore
24
24
  from statewire import Statewire, command, plain
25
25
  from statewire.langgraph import append_langgraph_event
26
26
 
@@ -154,36 +154,42 @@ class RunsHost(Statewire):
154
154
  await stream.aclose()
155
155
 
156
156
  @command("run/enqueue")
157
- async def run_enqueue(self, params, *, ctx):
158
- return await (await self.run.get()).enqueue(params, applied=ctx.applied)
157
+ async def run_enqueue(self, params, *, ctx, auth_result):
158
+ return await self.run.enqueue(
159
+ params, applied=ctx.applied, auth_result=auth_result
160
+ )
159
161
 
160
162
  @command("run/steer")
161
- async def run_steer(self, params, *, ctx):
162
- return await (await self.run.get()).steer(params, applied=ctx.applied)
163
+ async def run_steer(self, params, *, ctx, auth_result):
164
+ return await self.run.steer(
165
+ params, applied=ctx.applied, auth_result=auth_result
166
+ )
163
167
 
164
168
  @command("run/dequeue")
165
- async def run_dequeue(self, params):
166
- return await (await self.run.get()).dequeue(params)
169
+ async def run_dequeue(self, params, *, auth_result):
170
+ return await self.run.dequeue(params, auth_result=auth_result)
167
171
 
168
172
  @command("run/edit")
169
- async def run_edit(self, params, *, ctx):
170
- return await (await self.run.get()).edit(params, applied=ctx.applied)
173
+ async def run_edit(self, params, *, ctx, auth_result):
174
+ return await self.run.edit(params, applied=ctx.applied, auth_result=auth_result)
171
175
 
172
176
  @command("run/reload")
173
- async def run_reload(self, params, *, ctx):
174
- return await (await self.run.get()).reload(params, applied=ctx.applied)
177
+ async def run_reload(self, params, *, ctx, auth_result):
178
+ return await self.run.reload(
179
+ params, applied=ctx.applied, auth_result=auth_result
180
+ )
175
181
 
176
182
  @command("run/stop")
177
- async def run_stop(self, params=None, *, ctx):
178
- return await (await self.run.get()).stop(params, applied=ctx.applied)
183
+ async def run_stop(self, params=None, *, ctx, auth_result):
184
+ return await self.run.stop(params, applied=ctx.applied, auth_result=auth_result)
179
185
 
180
186
  @command("run/continue")
181
- async def run_continue(self, params=None, *, ctx):
182
- return await (await self.run.get()).continue_run(applied=ctx.applied)
187
+ async def run_continue(self, params=None, *, ctx, auth_result):
188
+ return await self.run.continue_run(applied=ctx.applied, auth_result=auth_result)
183
189
 
184
190
  @command("run/input")
185
- async def run_input(self, params):
186
- return await (await self.run.get()).input(params)
191
+ async def run_input(self, params, *, auth_result):
192
+ return await self.run.input(params, auth_result=auth_result)
187
193
 
188
194
 
189
195
  worker = PinnedWorker(
@@ -193,6 +199,6 @@ worker = PinnedWorker(
193
199
  advertise_url=os.environ.get("ADVERTISE_URL"),
194
200
  )
195
201
  router = PinnedHost(RunsHost, worker=worker, namespace="/threads")
196
- app = FastAPI(lifespan=router.lifespan)
202
+ app = FastAPI()
197
203
  app.include_router(worker)
198
204
  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.34.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.14.0,<0.15", "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,