harness-sdk-python 0.25.0__tar.gz → 0.25.2__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.25.0 → harness_sdk_python-0.25.2}/PKG-INFO +2 -2
  2. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/pyproject.toml +2 -2
  3. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/src/harness_sdk/checkpoints.py +74 -52
  4. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_checkpoints.py +22 -7
  5. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/.gitignore +0 -0
  6. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/README.md +0 -0
  7. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/examples/__init__.py +0 -0
  8. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/examples/runs_app.py +0 -0
  9. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/src/harness_sdk/__init__.py +0 -0
  10. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/src/harness_sdk/fenced_postgres.py +0 -0
  11. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/src/harness_sdk/linear_thread.py +0 -0
  12. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/src/harness_sdk/run_manager.py +0 -0
  13. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/run_helpers.py +0 -0
  14. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_admit_steer.py +0 -0
  15. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_applied_visibility.py +0 -0
  16. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_batches.py +0 -0
  17. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_branch_anchor.py +0 -0
  18. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_dispatching.py +0 -0
  19. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_durability.py +0 -0
  20. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_edit_dispatched.py +0 -0
  21. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_edit_reload.py +0 -0
  22. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_enqueue.py +0 -0
  23. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_fenced_postgres.py +0 -0
  24. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_input_required.py +0 -0
  25. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_linear_thread.py +0 -0
  26. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_meta.py +0 -0
  27. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_outcomes.py +0 -0
  28. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_placement.py +0 -0
  29. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_prepare_hooks.py +0 -0
  30. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_restore.py +0 -0
  31. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_rewind_during_run.py +0 -0
  32. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_run_crash_sweep.py +0 -0
  33. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_settle.py +0 -0
  34. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_steer.py +0 -0
  35. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/tests/test_stop_continue.py +0 -0
  36. {harness_sdk_python-0.25.0 → harness_sdk_python-0.25.2}/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.25.0
3
+ Version: 0.25.2
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.25.0"
3
+ version = "0.25.2"
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"]
@@ -11,6 +11,7 @@ Rows and entries are insert-only; ``last_checkpoint_id`` is the only update.
11
11
  """
12
12
 
13
13
  import asyncio
14
+ from contextlib import asynccontextmanager
14
15
  from dataclasses import dataclass, field
15
16
  from typing import Any, Callable, Protocol
16
17
 
@@ -158,50 +159,69 @@ class MemoryCheckpointIndex:
158
159
 
159
160
 
160
161
  class PostgresCheckpointIndex:
161
- """psycopg-backed CheckpointIndex; ``setup()`` creates the two tables."""
162
+ """psycopg-backed CheckpointIndex over an ``AsyncConnection`` or ``AsyncConnectionPool``; ``setup()`` creates the two tables."""
162
163
 
163
164
  def __init__(self, conn: Any) -> None:
164
165
  try:
165
- import psycopg # noqa: F401
166
+ import psycopg
167
+ import psycopg_pool
166
168
  except ImportError as exc:
167
169
  raise ImportError(
168
170
  "harness_sdk.checkpoints.PostgresCheckpointIndex requires the "
169
171
  "'postgres' extra: pip install 'harness-sdk-python[postgres]'"
170
172
  ) from exc
173
+ if not isinstance(
174
+ conn, (psycopg.AsyncConnection, psycopg_pool.AsyncConnectionPool)
175
+ ):
176
+ raise TypeError(
177
+ f"conn must be an AsyncConnection or AsyncConnectionPool, got {conn!r}"
178
+ )
171
179
  self._conn = conn
172
180
 
181
+ @asynccontextmanager
182
+ async def _connection(self):
183
+ import psycopg
184
+
185
+ if isinstance(self._conn, psycopg.AsyncConnection):
186
+ yield self._conn
187
+ else:
188
+ async with self._conn.connection() as conn:
189
+ yield conn
190
+
173
191
  async def setup(self) -> None:
174
- await self._conn.execute(
175
- "CREATE TABLE IF NOT EXISTS checkpoint_threads ("
176
- " thread_id TEXT NOT NULL,"
177
- " checkpoint_ns TEXT NOT NULL,"
178
- " branches JSONB NOT NULL,"
179
- " PRIMARY KEY (thread_id, checkpoint_ns))"
180
- )
181
- await self._conn.execute(
182
- "CREATE TABLE IF NOT EXISTS checkpoint_messages ("
183
- " thread_id TEXT NOT NULL,"
184
- " checkpoint_ns TEXT NOT NULL,"
185
- " message_id TEXT NOT NULL,"
186
- " anchor_checkpoint_id TEXT,"
187
- " branch_id INTEGER NOT NULL,"
188
- " PRIMARY KEY (thread_id, checkpoint_ns, message_id))"
189
- )
192
+ async with self._connection() as conn:
193
+ await conn.execute(
194
+ "CREATE TABLE IF NOT EXISTS checkpoint_threads ("
195
+ " thread_id TEXT NOT NULL,"
196
+ " checkpoint_ns TEXT NOT NULL,"
197
+ " branches JSONB NOT NULL,"
198
+ " PRIMARY KEY (thread_id, checkpoint_ns))"
199
+ )
200
+ await conn.execute(
201
+ "CREATE TABLE IF NOT EXISTS checkpoint_messages ("
202
+ " thread_id TEXT NOT NULL,"
203
+ " checkpoint_ns TEXT NOT NULL,"
204
+ " message_id TEXT NOT NULL,"
205
+ " anchor_checkpoint_id TEXT,"
206
+ " branch_id INTEGER NOT NULL,"
207
+ " PRIMARY KEY (thread_id, checkpoint_ns, message_id))"
208
+ )
190
209
 
191
210
  async def read_thread(
192
211
  self, thread_id: str, checkpoint_ns: str
193
212
  ) -> list[dict[str, Any]]:
194
- cur = await self._conn.execute(
195
- "SELECT branches FROM checkpoint_threads WHERE thread_id = %s AND checkpoint_ns = %s",
196
- (thread_id, checkpoint_ns),
197
- )
198
- row = await cur.fetchone()
213
+ async with self._connection() as conn:
214
+ cur = await conn.execute(
215
+ "SELECT branches FROM checkpoint_threads WHERE thread_id = %s AND checkpoint_ns = %s",
216
+ (thread_id, checkpoint_ns),
217
+ )
218
+ row = await cur.fetchone()
199
219
  return row[0] if row is not None else []
200
220
 
201
- async def _seed_root(self, thread_id: str, checkpoint_ns: str) -> None:
221
+ async def _seed_root(self, conn: Any, thread_id: str, checkpoint_ns: str) -> None:
202
222
  from psycopg.types.json import Jsonb
203
223
 
204
- await self._conn.execute(
224
+ await conn.execute(
205
225
  "INSERT INTO checkpoint_threads (thread_id, checkpoint_ns, branches)"
206
226
  " VALUES (%s, %s, %s) ON CONFLICT DO NOTHING",
207
227
  (thread_id, checkpoint_ns, Jsonb([_ROOT_ENTRY])),
@@ -223,9 +243,9 @@ class PostgresCheckpointIndex:
223
243
  "fork_branch": fork_branch,
224
244
  "last_checkpoint_id": last_checkpoint_id,
225
245
  }
226
- async with self._conn.transaction():
227
- await self._seed_root(thread_id, checkpoint_ns)
228
- cur = await self._conn.execute(
246
+ async with self._connection() as conn, conn.transaction():
247
+ await self._seed_root(conn, thread_id, checkpoint_ns)
248
+ cur = await conn.execute(
229
249
  "UPDATE checkpoint_threads SET branches = branches || %s"
230
250
  " WHERE thread_id = %s AND checkpoint_ns = %s"
231
251
  " RETURNING jsonb_array_length(branches)",
@@ -240,26 +260,27 @@ class PostgresCheckpointIndex:
240
260
  branch_ids: list[int],
241
261
  checkpoint_id: str,
242
262
  ) -> None:
243
- await self._seed_root(thread_id, checkpoint_ns)
244
- cur = await self._conn.execute(
245
- "SELECT jsonb_array_length(branches) FROM checkpoint_threads"
246
- " WHERE thread_id = %s AND checkpoint_ns = %s",
247
- (thread_id, checkpoint_ns),
248
- )
249
- row = await cur.fetchone()
250
- _check_branch_ids(branch_ids, row[0] if row is not None else 0)
251
- await self._conn.execute(
252
- "UPDATE checkpoint_threads SET branches = (SELECT jsonb_agg("
253
- " CASE WHEN idx::int - 1 = ANY(%s)"
254
- " THEN elem || jsonb_build_object('last_checkpoint_id', %s::text)"
255
- " ELSE elem END ORDER BY idx)"
256
- " FROM jsonb_array_elements(branches) WITH ORDINALITY AS t(elem, idx))"
257
- " WHERE thread_id = %s AND checkpoint_ns = %s",
258
- (branch_ids, checkpoint_id, thread_id, checkpoint_ns),
259
- )
263
+ async with self._connection() as conn:
264
+ await self._seed_root(conn, thread_id, checkpoint_ns)
265
+ cur = await conn.execute(
266
+ "SELECT jsonb_array_length(branches) FROM checkpoint_threads"
267
+ " WHERE thread_id = %s AND checkpoint_ns = %s",
268
+ (thread_id, checkpoint_ns),
269
+ )
270
+ row = await cur.fetchone()
271
+ _check_branch_ids(branch_ids, row[0] if row is not None else 0)
272
+ await conn.execute(
273
+ "UPDATE checkpoint_threads SET branches = (SELECT jsonb_agg("
274
+ " CASE WHEN idx::int - 1 = ANY(%s)"
275
+ " THEN elem || jsonb_build_object('last_checkpoint_id', %s::text)"
276
+ " ELSE elem END ORDER BY idx)"
277
+ " FROM jsonb_array_elements(branches) WITH ORDINALITY AS t(elem, idx))"
278
+ " WHERE thread_id = %s AND checkpoint_ns = %s",
279
+ (branch_ids, checkpoint_id, thread_id, checkpoint_ns),
280
+ )
260
281
 
261
282
  async def upsert_messages(self, rows: list[dict[str, Any]]) -> None:
262
- async with self._conn.cursor() as cur:
283
+ async with self._connection() as conn, conn.cursor() as cur:
263
284
  await cur.executemany(
264
285
  "INSERT INTO checkpoint_messages"
265
286
  " (thread_id, checkpoint_ns, message_id, anchor_checkpoint_id, branch_id)"
@@ -272,12 +293,13 @@ class PostgresCheckpointIndex:
272
293
  async def get_message(
273
294
  self, thread_id: str, checkpoint_ns: str, message_id: str
274
295
  ) -> dict[str, Any] | None:
275
- cur = await self._conn.execute(
276
- "SELECT anchor_checkpoint_id, branch_id FROM checkpoint_messages"
277
- " WHERE thread_id = %s AND checkpoint_ns = %s AND message_id = %s",
278
- (thread_id, checkpoint_ns, message_id),
279
- )
280
- row = await cur.fetchone()
296
+ async with self._connection() as conn:
297
+ cur = await conn.execute(
298
+ "SELECT anchor_checkpoint_id, branch_id FROM checkpoint_messages"
299
+ " WHERE thread_id = %s AND checkpoint_ns = %s AND message_id = %s",
300
+ (thread_id, checkpoint_ns, message_id),
301
+ )
302
+ row = await cur.fetchone()
281
303
  if row is None:
282
304
  return None
283
305
  return {"anchor_checkpoint_id": row[0], "branch_id": row[1]}
@@ -663,11 +663,12 @@ def postgres_dsn():
663
663
  subprocess.run([docker, "rm", "-f", container], capture_output=True)
664
664
 
665
665
 
666
- @pytest.fixture
667
- async def pg_index(postgres_dsn):
666
+ @pytest.fixture(params=["conn", "pool"])
667
+ async def pg_index(request, postgres_dsn):
668
668
  import uuid
669
669
 
670
670
  import psycopg
671
+ from psycopg_pool import AsyncConnectionPool
671
672
 
672
673
  from harness_sdk import PostgresCheckpointIndex
673
674
 
@@ -675,15 +676,29 @@ async def pg_index(postgres_dsn):
675
676
  with psycopg.connect(postgres_dsn, autocommit=True) as admin:
676
677
  admin.execute(f'CREATE DATABASE "{dbname}"')
677
678
  dsn = postgres_dsn.rsplit("/", 1)[0] + f"/{dbname}"
678
- conn = await psycopg.AsyncConnection.connect(dsn, autocommit=True)
679
- index = PostgresCheckpointIndex(conn)
680
- await index.setup()
681
- yield index
682
- await conn.close()
679
+ if request.param == "conn":
680
+ conn = await psycopg.AsyncConnection.connect(dsn, autocommit=True)
681
+ index = PostgresCheckpointIndex(conn)
682
+ await index.setup()
683
+ yield index
684
+ await conn.close()
685
+ else:
686
+ async with AsyncConnectionPool(dsn, open=False) as pool:
687
+ index = PostgresCheckpointIndex(pool)
688
+ await index.setup()
689
+ yield index
683
690
  with psycopg.connect(postgres_dsn, autocommit=True) as admin:
684
691
  admin.execute(f'DROP DATABASE "{dbname}" WITH (FORCE)')
685
692
 
686
693
 
694
+ def test_postgres_index_rejects_other_conn_types():
695
+ pytest.importorskip("psycopg")
696
+ from harness_sdk import PostgresCheckpointIndex
697
+
698
+ with pytest.raises(TypeError, match="AsyncConnection or AsyncConnectionPool"):
699
+ PostgresCheckpointIndex(object())
700
+
701
+
687
702
  async def test_postgres_branches_round_trip(pg_index):
688
703
  assert await pg_index.read_thread(THREAD, "") == []
689
704
  first = await pg_index.append_branch(