graphharbor 0.13.0.post34__tar.gz → 0.13.0.post35__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.
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/PKG-INFO +2 -2
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/pyproject.toml +2 -2
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/src/langhost/protocol_api.py +27 -1
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/tests/test_stream_heartbeat_resilience.py +81 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/.gitignore +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/LICENSE +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/README.md +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/src/langhost/__init__.py +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/src/langhost/__main__.py +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/src/langhost/cli.py +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/src/langhost/core_api.py +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/src/langhost/mcp_transport.py +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/src/langhost/server.py +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/src/langhost/store_api.py +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/src/langhost/streaming.py +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/tests/test_application_authorization.py +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/tests/test_cli.py +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/tests/test_graph_discovery.py +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/tests/test_mcp_transport.py +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/tests/test_official_protocol_compare.py +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/tests/test_server_paths.py +0 -0
- {graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/tests/test_thread_state_projection.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: graphharbor
|
|
3
|
-
Version: 0.13.0.
|
|
3
|
+
Version: 0.13.0.post35
|
|
4
4
|
Summary: GraphHarbor CLI for a self-hosted LangGraph Agent Server on PostgreSQL and Redis
|
|
5
5
|
Project-URL: Homepage, https://github.com/ljxpython/graphharbor
|
|
6
6
|
Project-URL: Repository, https://github.com/ljxpython/graphharbor
|
|
@@ -26,7 +26,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
26
26
|
Classifier: Typing :: Typed
|
|
27
27
|
Requires-Python: >=3.11
|
|
28
28
|
Requires-Dist: click>=8.1.7
|
|
29
|
-
Requires-Dist: graphharbor-runtime==0.13.0.
|
|
29
|
+
Requires-Dist: graphharbor-runtime==0.13.0.post35
|
|
30
30
|
Requires-Dist: langgraph-cli<0.5,>=0.4.0
|
|
31
31
|
Requires-Dist: mcp<2,>=1.23
|
|
32
32
|
Requires-Dist: pyfiglet>=1.0.0
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "graphharbor"
|
|
3
3
|
# Lockstep with graphharbor-runtime; independent of langgraph-api releases.
|
|
4
|
-
version = "0.13.0.
|
|
4
|
+
version = "0.13.0.post35"
|
|
5
5
|
description = "GraphHarbor CLI for a self-hosted LangGraph Agent Server on PostgreSQL and Redis"
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "MIT"
|
|
@@ -50,7 +50,7 @@ dependencies = [
|
|
|
50
50
|
"mcp>=1.23,<2",
|
|
51
51
|
"uvicorn[standard]>=0.51.0",
|
|
52
52
|
# Exact same version as graphharbor-runtime (lockstep releases).
|
|
53
|
-
"graphharbor-runtime==0.13.0.
|
|
53
|
+
"graphharbor-runtime==0.13.0.post35",
|
|
54
54
|
]
|
|
55
55
|
|
|
56
56
|
[project.urls]
|
|
@@ -189,6 +189,12 @@ async def protocol_commands(request: Request) -> JSONResponse:
|
|
|
189
189
|
key: value for key, value in persisted.interrupts.items() if key != interrupt_id
|
|
190
190
|
}
|
|
191
191
|
persisted.status = "busy"
|
|
192
|
+
old_run = await conn.session.get(RunRow, latest.run_id)
|
|
193
|
+
if old_run is not None:
|
|
194
|
+
old_kwargs = dict(old_run.kwargs or {})
|
|
195
|
+
if old_kwargs.get("stream_resumable") is True:
|
|
196
|
+
old_kwargs["stream_resumable"] = False
|
|
197
|
+
old_run.kwargs = old_kwargs
|
|
192
198
|
return JSONResponse(
|
|
193
199
|
{
|
|
194
200
|
"id": command_id,
|
|
@@ -321,10 +327,30 @@ async def protocol_event_stream(request: Request) -> JSONResponse | StreamingRes
|
|
|
321
327
|
if (uid := _safe_uuid(wire.get("params", {}).get("run_id"))) is not None
|
|
322
328
|
}
|
|
323
329
|
resumable = await _resumable_run_ids(parsed_run_ids)
|
|
330
|
+
active_interrupt_ids = (
|
|
331
|
+
set(thread.interrupts.keys()) if isinstance(thread.interrupts, dict) else set()
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def _is_active_protocol_event(w: dict[str, Any]) -> bool:
|
|
335
|
+
method = w.get("method")
|
|
336
|
+
params = w.get("params") or {}
|
|
337
|
+
data = params.get("data") or {}
|
|
338
|
+
is_input_req = (
|
|
339
|
+
method == "input.requested"
|
|
340
|
+
or (method == "input" and data.get("event") == "requested")
|
|
341
|
+
or data.get("event") == "input.requested"
|
|
342
|
+
)
|
|
343
|
+
if is_input_req:
|
|
344
|
+
iid = str(data.get("interrupt_id") or params.get("interrupt_id") or "")
|
|
345
|
+
if iid and iid not in active_interrupt_ids:
|
|
346
|
+
return False
|
|
347
|
+
return True
|
|
348
|
+
|
|
324
349
|
replay = [
|
|
325
350
|
wire
|
|
326
351
|
for wire in replay
|
|
327
|
-
if (uid := _safe_uuid(wire.get("params", {}).get("run_id"))) is None or uid in resumable
|
|
352
|
+
if ((uid := _safe_uuid(wire.get("params", {}).get("run_id"))) is None or uid in resumable)
|
|
353
|
+
and _is_active_protocol_event(wire)
|
|
328
354
|
]
|
|
329
355
|
except Exception:
|
|
330
356
|
await manager.remove_thread_stream(thread_id, queue)
|
{graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/tests/test_stream_heartbeat_resilience.py
RENAMED
|
@@ -261,3 +261,84 @@ async def test_stream_response_headers_compliance(monkeypatch):
|
|
|
261
261
|
assert "no-cache" in res_thread.headers["cache-control"]
|
|
262
262
|
assert res_thread.headers["x-accel-buffering"] == "no"
|
|
263
263
|
await res_thread.body_iterator.aclose()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@pytest.mark.asyncio
|
|
267
|
+
async def test_zombie_interrupt_replay_filtered(monkeypatch):
|
|
268
|
+
"""Ensure resolved/historical input.requested events are filtered from replay."""
|
|
269
|
+
thread_id = uuid4()
|
|
270
|
+
run_id = uuid4()
|
|
271
|
+
|
|
272
|
+
# Active interrupts on thread only contains 'int-active'
|
|
273
|
+
mock_thread = ThreadRow(
|
|
274
|
+
thread_id=thread_id,
|
|
275
|
+
status="interrupted",
|
|
276
|
+
metadata_={},
|
|
277
|
+
config={},
|
|
278
|
+
values_={},
|
|
279
|
+
interrupts={"int-active": {"id": "int-active", "value": "Approve step 2"}},
|
|
280
|
+
error=None,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
manager = InMemoryStreamManager()
|
|
284
|
+
monkeypatch.setattr("langhost.protocol_api.get_stream_manager", lambda: manager)
|
|
285
|
+
monkeypatch.setattr(
|
|
286
|
+
"langhost.protocol_api._thread",
|
|
287
|
+
lambda *a, **k: asyncio.sleep(0, result=mock_thread),
|
|
288
|
+
)
|
|
289
|
+
monkeypatch.setattr(
|
|
290
|
+
"langhost.streaming._resumable_run_ids",
|
|
291
|
+
lambda run_ids: asyncio.sleep(0, result=set(run_ids)),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# 2 historical events: seq 1 is zombie (int-old), seq 2 is active (int-active)
|
|
295
|
+
wire_old = protocol_event(
|
|
296
|
+
event_id="evt-1",
|
|
297
|
+
sequence=1,
|
|
298
|
+
run_id=str(run_id),
|
|
299
|
+
thread_id=str(thread_id),
|
|
300
|
+
event={
|
|
301
|
+
"event": "input.requested",
|
|
302
|
+
"data": {"interrupt_id": "int-old", "value": "Old resolved request"},
|
|
303
|
+
},
|
|
304
|
+
)
|
|
305
|
+
wire_active = protocol_event(
|
|
306
|
+
event_id="evt-2",
|
|
307
|
+
sequence=2,
|
|
308
|
+
run_id=str(run_id),
|
|
309
|
+
thread_id=str(thread_id),
|
|
310
|
+
event={
|
|
311
|
+
"event": "input.requested",
|
|
312
|
+
"data": {"interrupt_id": "int-active", "value": "Active request"},
|
|
313
|
+
},
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
monkeypatch.setattr(
|
|
317
|
+
"langhost.protocol_api._load_protocol_events",
|
|
318
|
+
lambda *a, **k: asyncio.sleep(0, result=(0, [wire_old, wire_active])),
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
scope, receive = _make_scope_and_receive(
|
|
322
|
+
"POST",
|
|
323
|
+
f"/threads/{thread_id}/stream/events",
|
|
324
|
+
{"thread_id": str(thread_id)},
|
|
325
|
+
body_dict={"channels": ["input"], "since": 0},
|
|
326
|
+
)
|
|
327
|
+
req = Request(scope, receive)
|
|
328
|
+
res = await protocol_event_stream(req)
|
|
329
|
+
assert isinstance(res, StreamingResponse)
|
|
330
|
+
|
|
331
|
+
frames: list[str] = []
|
|
332
|
+
try:
|
|
333
|
+
async for chunk in res.body_iterator:
|
|
334
|
+
frames.append(chunk)
|
|
335
|
+
# Replay emits immediately, break after replay
|
|
336
|
+
if "int-active" in chunk or len(frames) >= 2:
|
|
337
|
+
break
|
|
338
|
+
finally:
|
|
339
|
+
await res.body_iterator.aclose()
|
|
340
|
+
|
|
341
|
+
all_output = "".join(frames)
|
|
342
|
+
assert "int-old" not in all_output, "Zombie interrupt 'int-old' must NOT be replayed to client!"
|
|
343
|
+
assert "int-active" in all_output, "Active interrupt 'int-active' must be replayed to client!"
|
|
344
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/tests/test_application_authorization.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/tests/test_official_protocol_compare.py
RENAMED
|
File without changes
|
|
File without changes
|
{graphharbor-0.13.0.post34 → graphharbor-0.13.0.post35}/tests/test_thread_state_projection.py
RENAMED
|
File without changes
|