harness-sdk-python 0.32.1__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.
- harness_sdk_python-0.33.0/PKG-INFO +62 -0
- harness_sdk_python-0.33.0/README.md +43 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/examples/runs_app.py +1 -1
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/pyproject.toml +2 -2
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/src/harness_sdk/__init__.py +19 -1
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/src/harness_sdk/checkpoints.py +83 -18
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/src/harness_sdk/deepagents.py +145 -75
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/src/harness_sdk/run_manager.py +160 -51
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/src/harness_sdk/voice.py +101 -19
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/run_helpers.py +29 -17
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_admit_steer.py +4 -2
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_checkpoints.py +67 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_deepagents_messages.py +128 -1
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_fenced_postgres.py +1 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_input_required.py +66 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_mint_concurrency.py +6 -3
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_outcomes.py +33 -1
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_run_crash_sweep.py +39 -18
- harness_sdk_python-0.33.0/tests/test_run_ownership.py +261 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_settle.py +7 -3
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_steer.py +38 -1
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_stop_continue.py +3 -1
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_voice.py +205 -4
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_voice_delegation.py +107 -4
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/voice_helpers.py +9 -1
- harness_sdk_python-0.32.1/PKG-INFO +0 -66
- harness_sdk_python-0.32.1/README.md +0 -47
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/.gitignore +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/examples/__init__.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/src/harness_sdk/linear_thread.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_applied_visibility.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_batches.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_branch_anchor.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_dispatching.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_durability.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_edit_dispatched.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_edit_reload.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_enqueue.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_linear_thread.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_meta.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_placement.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_prepare_hooks.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_restore.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_rewind_during_run.py +0 -0
- {harness_sdk_python-0.32.1 → harness_sdk_python-0.33.0}/tests/test_stopping_window.py +0 -0
- {harness_sdk_python-0.32.1 → 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(
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
336
|
+
messages=[_message_dict(m) for m in messages],
|
|
332
337
|
lineage=lineage,
|
|
333
338
|
)
|
|
334
339
|
|
|
335
340
|
|
|
336
|
-
def
|
|
337
|
-
|
|
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
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
667
|
-
|
|
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,
|