langgraph-api 0.0.15__py3-none-any.whl → 0.0.16__py3-none-any.whl

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.

Potentially problematic release.


This version of langgraph-api might be problematic. Click here for more details.

@@ -1,7 +1,12 @@
1
+ from typing import Any
2
+
3
+ from langgraph_sdk.auth import Auth
1
4
  from starlette.responses import Response
2
5
  from starlette.routing import BaseRoute
3
6
 
7
+ from langgraph_api.auth.custom import handle_event as _handle_event
4
8
  from langgraph_api.route import ApiRequest, ApiResponse, ApiRoute
9
+ from langgraph_api.utils import get_auth_ctx
5
10
  from langgraph_api.validation import (
6
11
  StoreDeleteRequest,
7
12
  StoreListNamespacesRequest,
@@ -21,6 +26,24 @@ def _validate_namespace(namespace: tuple[str, ...]) -> Response | None:
21
26
  )
22
27
 
23
28
 
29
+ async def handle_event(
30
+ action: str,
31
+ value: Any,
32
+ ) -> None:
33
+ ctx = get_auth_ctx()
34
+ if not ctx:
35
+ return
36
+ await _handle_event(
37
+ Auth.types.AuthContext(
38
+ user=ctx.user,
39
+ permissions=ctx.permissions,
40
+ resource="store",
41
+ action=action,
42
+ ),
43
+ value,
44
+ )
45
+
46
+
24
47
  @retry_db
25
48
  async def put_item(request: ApiRequest):
26
49
  """Store or update an item."""
@@ -28,9 +51,13 @@ async def put_item(request: ApiRequest):
28
51
  namespace = tuple(payload["namespace"]) if payload.get("namespace") else ()
29
52
  if err := _validate_namespace(namespace):
30
53
  return err
31
- key = payload["key"]
32
- value = payload["value"]
33
- await Store().aput(namespace, key, value)
54
+ handler_payload = {
55
+ "namespace": namespace,
56
+ "key": payload["key"],
57
+ "value": payload["value"],
58
+ }
59
+ await handle_event("put", handler_payload)
60
+ await Store().aput(namespace, handler_payload["key"], handler_payload["value"])
34
61
  return Response(status_code=204)
35
62
 
36
63
 
@@ -43,6 +70,11 @@ async def get_item(request: ApiRequest):
43
70
  key = request.query_params.get("key")
44
71
  if not key:
45
72
  return ApiResponse({"error": "Key is required"}, status_code=400)
73
+ handler_payload = {
74
+ "namespace": namespace,
75
+ "key": key,
76
+ }
77
+ await handle_event("get", handler_payload)
46
78
  result = await Store().aget(namespace, key)
47
79
  return ApiResponse(result.dict() if result is not None else None)
48
80
 
@@ -54,8 +86,12 @@ async def delete_item(request: ApiRequest):
54
86
  namespace = tuple(payload["namespace"]) if payload.get("namespace") else ()
55
87
  if err := _validate_namespace(namespace):
56
88
  return err
57
- key = payload["key"]
58
- await Store().adelete(namespace, key)
89
+ handler_payload = {
90
+ "namespace": namespace,
91
+ "key": payload["key"],
92
+ }
93
+ await handle_event("delete", handler_payload)
94
+ await Store().adelete(handler_payload["namespace"], handler_payload["key"])
59
95
  return Response(status_code=204)
60
96
 
61
97
 
@@ -70,12 +106,20 @@ async def search_items(request: ApiRequest):
70
106
  limit = payload.get("limit") or 10
71
107
  offset = payload.get("offset") or 0
72
108
  query = payload.get("query")
109
+ handler_payload = {
110
+ "namespace": namespace_prefix,
111
+ "filter": filter,
112
+ "limit": limit,
113
+ "offset": offset,
114
+ "query": query,
115
+ }
116
+ await handle_event("search", handler_payload)
73
117
  items = await Store().asearch(
74
- namespace_prefix,
75
- filter=filter,
76
- limit=limit,
77
- offset=offset,
78
- query=query,
118
+ handler_payload["namespace"],
119
+ filter=handler_payload["filter"],
120
+ limit=handler_payload["limit"],
121
+ offset=handler_payload["offset"],
122
+ query=handler_payload["query"],
79
123
  )
80
124
  return ApiResponse({"items": [item.dict() for item in items]})
81
125
 
@@ -93,12 +137,20 @@ async def list_namespaces(request: ApiRequest):
93
137
  max_depth = payload.get("max_depth")
94
138
  limit = payload.get("limit", 100)
95
139
  offset = payload.get("offset", 0)
140
+ handler_payload = {
141
+ "namespace": prefix,
142
+ "suffix": suffix,
143
+ "max_depth": max_depth,
144
+ "limit": limit,
145
+ "offset": offset,
146
+ }
147
+ await handle_event("list_namespaces", handler_payload)
96
148
  result = await Store().alist_namespaces(
97
- prefix=prefix,
98
- suffix=suffix,
99
- max_depth=max_depth,
100
- limit=limit,
101
- offset=offset,
149
+ prefix=handler_payload["namespace"],
150
+ suffix=handler_payload["suffix"],
151
+ max_depth=handler_payload["max_depth"],
152
+ limit=handler_payload["limit"],
153
+ offset=handler_payload["offset"],
102
154
  )
103
155
  return ApiResponse({"namespaces": result})
104
156
 
langgraph_api/asyncio.py CHANGED
@@ -60,6 +60,11 @@ async def wait_if_not_done(coro: Coroutine[Any, Any, T], done: ValueEvent) -> T:
60
60
  try:
61
61
  return await coro_task
62
62
  except asyncio.CancelledError as e:
63
+ if e.args and asyncio.isfuture(e.args[-1]):
64
+ await logger.ainfo(
65
+ "Awaiting future upon cancellation", task=str(e.args[-1])
66
+ )
67
+ await e.args[-1]
63
68
  if e.args and isinstance(e.args[0], Exception):
64
69
  raise e.args[0] from None
65
70
  raise
@@ -398,6 +398,16 @@ class DotDict:
398
398
  raise AttributeError(f"'DotDict' object has no attribute '{name}'")
399
399
  return self._dict[name]
400
400
 
401
+ def __getitem__(self, key):
402
+ return self._dict[key]
403
+
404
+ def __setitem__(self, key, value):
405
+ self._dict[key] = value
406
+ if isinstance(value, dict):
407
+ setattr(self, key, DotDict(value))
408
+ else:
409
+ setattr(self, key, value)
410
+
401
411
  def __deepcopy__(self, memo):
402
412
  return DotDict(copy.deepcopy(self._dict))
403
413
 
@@ -457,6 +467,12 @@ class ProxyUser(BaseUser):
457
467
  **d,
458
468
  }
459
469
 
470
+ def __getitem__(self, key):
471
+ return self._user[key]
472
+
473
+ def __setitem__(self, key, value):
474
+ self._user[key] = value
475
+
460
476
  def __getattr__(self, name: str) -> Any:
461
477
  """Proxy any other attributes to the underlying user object."""
462
478
  return getattr(self._user, name)
@@ -481,10 +497,10 @@ def _normalize_auth_response(
481
497
  user = response
482
498
  permissions = []
483
499
 
484
- return AuthCredentials(permissions), _normalize_user(user)
500
+ return AuthCredentials(permissions), normalize_user(user)
485
501
 
486
502
 
487
- def _normalize_user(user: Any) -> BaseUser:
503
+ def normalize_user(user: Any) -> BaseUser:
488
504
  """Normalize user into a BaseUser instance."""
489
505
  if isinstance(user, BaseUser):
490
506
  return user
@@ -571,9 +587,8 @@ def _get_handler(auth: Auth, ctx: Auth.types.AuthContext) -> Auth.types.Handler
571
587
  ]
572
588
  for key in keys:
573
589
  if key in auth._handlers:
574
- result = auth._handlers[key][
575
- -1
576
- ] # Get the last defined, most specific handler
590
+ # Get the last defined, most specific handler
591
+ result = auth._handlers[key][-1]
577
592
  auth._handler_cache[key] = result
578
593
  return result
579
594
  if auth._global_handlers:
@@ -10,7 +10,7 @@
10
10
  "@hono/node-server": "^1.12.0",
11
11
  "@hono/zod-validator": "^0.2.2",
12
12
  "@langchain/core": "^0.3.22",
13
- "@langchain/langgraph": "^0.2.31",
13
+ "@langchain/langgraph": "^0.2.35",
14
14
  "@types/json-schema": "^7.0.15",
15
15
  "@typescript/vfs": "^1.6.0",
16
16
  "dedent": "^1.5.3",
@@ -23,6 +23,7 @@ from langgraph.pregel.types import PregelTask, StateSnapshot
23
23
  from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
24
24
  from langgraph.types import Command, Interrupt
25
25
  from pydantic import BaseModel
26
+ from starlette.exceptions import HTTPException
26
27
  from zmq.utils.monitor import recv_monitor_message
27
28
 
28
29
  from langgraph_api.js.base import BaseRemotePregel
@@ -681,5 +682,12 @@ async def js_healthcheck():
681
682
  try:
682
683
  await _client_invoke("ok", {})
683
684
  return True
684
- except (RemoteException, TimeoutError, zmq.error.ZMQBaseError):
685
- return False
685
+ except (RemoteException, TimeoutError, zmq.error.ZMQBaseError) as exc:
686
+ logger.warning(
687
+ "JS healthcheck failed. Either the JS server is not running or the event loop is blocked by a CPU-intensive task.",
688
+ error=exc,
689
+ )
690
+ raise HTTPException(
691
+ status_code=500,
692
+ detail="JS healthcheck failed. Either the JS server is not running or the event loop is blocked by a CPU-intensive task.",
693
+ ) from exc
@@ -22,6 +22,7 @@ from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
22
22
  from langgraph.types import Command, Interrupt
23
23
  from pydantic import BaseModel
24
24
  from starlette.applications import Starlette
25
+ from starlette.exceptions import HTTPException
25
26
  from starlette.requests import Request
26
27
  from starlette.routing import Route
27
28
 
@@ -653,5 +654,12 @@ async def js_healthcheck():
653
654
  res = await checkpointer_client.get("/ok")
654
655
  res.raise_for_status()
655
656
  return True
656
- except httpx.HTTPError:
657
- return False
657
+ except httpx.HTTPError as exc:
658
+ logger.warning(
659
+ "JS healthcheck failed. Either the JS server is not running or the event loop is blocked by a CPU-intensive task.",
660
+ error=exc,
661
+ )
662
+ raise HTTPException(
663
+ status_code=500,
664
+ detail="JS healthcheck failed. Either the JS server is not running or the event loop is blocked by a CPU-intensive task.",
665
+ ) from exc
@@ -753,13 +753,13 @@ describe("runs", () => {
753
753
  messages = findLast(chunks, (i) => i.event === "values")?.data.messages;
754
754
 
755
755
  const threadAfterInterrupt = await client.threads.get(thread.thread_id);
756
- expect(threadAfterInterrupt.status).toBe("idle");
756
+ expect(threadAfterInterrupt.status).toBe("interrupted");
757
757
 
758
758
  expect(messages.at(-1)).not.toBeNull();
759
- expect(messages.at(-1)?.content).toBe("end");
759
+ expect(messages.at(-1)?.content).toBe("begin");
760
760
 
761
761
  const state = await client.threads.getState(thread.thread_id);
762
- expect(state.next).toEqual([]);
762
+ expect(state.next).toEqual(["tool"]);
763
763
 
764
764
  // continue after interrupt
765
765
  chunks = await gatherIterator(
@@ -817,6 +817,7 @@ describe("runs", () => {
817
817
  });
818
818
 
819
819
  const modifiedThread = await client.threads.get(thread.thread_id);
820
+ expect(modifiedThread.status).toBe("interrupted");
820
821
  expect(modifiedThread.metadata?.modified).toBe(true);
821
822
 
822
823
  const stateAfterModify = await client.threads.getState<AgentState>(
@@ -836,22 +837,42 @@ describe("runs", () => {
836
837
  })
837
838
  );
838
839
 
840
+ const threadAfterContinue = await client.threads.get(thread.thread_id);
841
+ expect(threadAfterContinue.status).toBe("idle");
842
+
839
843
  expect(chunks.filter((i) => i.event === "error").length).toBe(0);
840
844
  messages = findLast(chunks, (i) => i.event === "values")?.data.messages;
841
845
 
842
- expect(messages.length).toBe(8);
843
- expect(messages[4].content).toBe(`tool_call__modified`);
846
+ expect(messages.length).toBe(4);
847
+ expect(messages[2].content).toBe(`tool_call__modified`);
844
848
  expect(messages.at(-1)?.content).toBe("end");
845
849
 
846
850
  // get the history
847
851
  const history = await client.threads.getHistory<AgentState>(
848
852
  thread.thread_id
849
853
  );
850
- expect(history.length).toBe(10);
854
+ expect(history.length).toBe(6);
851
855
  expect(history[0].next.length).toBe(0);
852
- expect(history[0].values.messages.length).toBe(8);
856
+ expect(history[0].values.messages.length).toBe(4);
853
857
  expect(history.at(-1)?.next).toEqual(["__start__"]);
854
858
  });
859
+
860
+ it.concurrent("interrupt before", async () => {
861
+ const assistant = await client.assistants.create({ graphId: "agent" });
862
+ let thread = await client.threads.create();
863
+ const input = {
864
+ messages: [{ type: "human", content: "foo", id: "initial-message" }],
865
+ };
866
+
867
+ await client.runs.wait(thread.thread_id, assistant.assistant_id, {
868
+ input,
869
+ interruptBefore: ["agent"],
870
+ config: globalConfig,
871
+ });
872
+
873
+ thread = await client.threads.get(thread.thread_id);
874
+ expect(thread.status).toBe("interrupted");
875
+ });
855
876
  });
856
877
 
857
878
  describe("shared state", () => {
@@ -1684,3 +1705,65 @@ describe("long running tasks", () => {
1684
1705
  }
1685
1706
  );
1686
1707
  });
1708
+
1709
+ // Not implemented in JS yet
1710
+ describe.skip("command update state", () => {
1711
+ it("updates state via commands", async () => {
1712
+ const assistant = await client.assistants.create({ graphId: "agent" });
1713
+ const thread = await client.threads.create();
1714
+
1715
+ const input = { messages: [{ role: "human", content: "foo" }] };
1716
+
1717
+ // dict-based updates
1718
+ await client.runs.wait(thread.thread_id, assistant.assistant_id, {
1719
+ input,
1720
+ config: globalConfig,
1721
+ });
1722
+ let stream = await gatherIterator(
1723
+ client.runs.stream(thread.thread_id, assistant.assistant_id, {
1724
+ command: { update: { keyOne: "value3", keyTwo: "value4" } },
1725
+ config: globalConfig,
1726
+ })
1727
+ );
1728
+ expect(stream.filter((chunk) => chunk.event === "error")).toEqual([]);
1729
+
1730
+ let state = await client.threads.getState<{
1731
+ keyOne: string;
1732
+ keyTwo: string;
1733
+ }>(thread.thread_id);
1734
+
1735
+ expect(state.values).toMatchObject({
1736
+ keyOne: "value3",
1737
+ keyTwo: "value4",
1738
+ });
1739
+
1740
+ // list-based updates
1741
+ await client.runs.wait(thread.thread_id, assistant.assistant_id, {
1742
+ input,
1743
+ config: globalConfig,
1744
+ });
1745
+ stream = await gatherIterator(
1746
+ client.runs.stream(thread.thread_id, assistant.assistant_id, {
1747
+ command: {
1748
+ update: [
1749
+ ["keyOne", "value1"],
1750
+ ["keyTwo", "value2"],
1751
+ ],
1752
+ },
1753
+ config: globalConfig,
1754
+ })
1755
+ );
1756
+
1757
+ expect(stream.filter((chunk) => chunk.event === "error")).toEqual([]);
1758
+
1759
+ state = await client.threads.getState<{
1760
+ keyOne: string;
1761
+ keyTwo: string;
1762
+ }>(thread.thread_id);
1763
+
1764
+ expect(state.values).toMatchObject({
1765
+ keyOne: "value1",
1766
+ keyTwo: "value2",
1767
+ });
1768
+ });
1769
+ });
@@ -18,6 +18,8 @@ const GraphAnnotationOutput = Annotation.Root({
18
18
  }),
19
19
  sharedStateValue: Annotation<string | null>(),
20
20
  interrupt: Annotation<boolean>(),
21
+ keyOne: Annotation<string | null>(),
22
+ keyTwo: Annotation<string | null>(),
21
23
  });
22
24
 
23
25
  const GraphAnnotationInput = Annotation.Root({
@@ -14,6 +14,11 @@ const StateSchema = Annotation.Root({
14
14
  const longRunning = async (
15
15
  state: typeof StateSchema.State
16
16
  ): Promise<typeof StateSchema.Update> => {
17
+ if (state.delay === -1) {
18
+ while (true) {
19
+ // hang the event loop
20
+ }
21
+ }
17
22
  await new Promise((resolve) => setTimeout(resolve, state.delay));
18
23
  return { messages: [`finished after ${state.delay}ms`] };
19
24
  };
@@ -389,10 +389,10 @@
389
389
  p-retry "4"
390
390
  uuid "^9.0.0"
391
391
 
392
- "@langchain/langgraph@^0.2.31":
393
- version "0.2.31"
394
- resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-0.2.31.tgz#2cb2faac1d02a7ccf47559aec87aae7f44dc7d3f"
395
- integrity sha512-/otJC3/P3Pt58eVZz1gxC3sBiC0N0HhOaAbOBKxckskhayBO6OC6ZDHtH9a+rxEIlreBoninR1/At1Gj/3liFA==
392
+ "@langchain/langgraph@^0.2.35":
393
+ version "0.2.35"
394
+ resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-0.2.35.tgz#e2dc4d07b3080570ef63b1a5a98a4e2b1e1cc630"
395
+ integrity sha512-h209sOZGgbKpdkc+5WgjiBH0Fe8zLmPv+ff/RnXGEr+phrXwUNQnx5iu4HexVd7P6gxM9Ymt1iZBCBXpgRbK8A==
396
396
  dependencies:
397
397
  "@langchain/langgraph-checkpoint" "~0.0.13"
398
398
  "@langchain/langgraph-sdk" "~0.0.21"
@@ -191,6 +191,7 @@ async def create_valid_run(
191
191
  user_id = get_user_id(user)
192
192
  config["configurable"]["langgraph_auth_user"] = user
193
193
  config["configurable"]["langgraph_auth_user_id"] = user_id
194
+ config["configurable"]["langgraph_auth_permissions"] = ctx.permissions
194
195
  else:
195
196
  user_id = None
196
197
  run_coro = Runs.put(
langgraph_api/queue.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
- from contextlib import AsyncExitStack
2
+ from collections.abc import AsyncGenerator
3
+ from contextlib import AsyncExitStack, asynccontextmanager
3
4
  from datetime import UTC, datetime
4
5
  from random import random
5
6
  from typing import TypedDict, cast
@@ -7,6 +8,7 @@ from typing import TypedDict, cast
7
8
  import structlog
8
9
  from langgraph.pregel.debug import CheckpointPayload, TaskResultPayload
9
10
 
11
+ from langgraph_api.auth.custom import normalize_user
10
12
  from langgraph_api.config import BG_JOB_NO_DELAY, STATS_INTERVAL_SECS
11
13
  from langgraph_api.errors import (
12
14
  UserInterrupt,
@@ -20,11 +22,16 @@ from langgraph_api.stream import (
20
22
  astream_state,
21
23
  consume,
22
24
  )
23
- from langgraph_api.utils import AsyncConnectionProto
25
+ from langgraph_api.utils import AsyncConnectionProto, set_auth_ctx, with_user
24
26
  from langgraph_storage.database import connect
25
27
  from langgraph_storage.ops import Runs, Threads
26
28
  from langgraph_storage.retry import RETRIABLE_EXCEPTIONS
27
29
 
30
+ try:
31
+ from psycopg.errors import InFailedSqlTransaction
32
+ except ImportError:
33
+ InFailedSqlTransaction = ()
34
+
28
35
  logger = structlog.stdlib.get_logger(__name__)
29
36
 
30
37
  WORKERS: set[asyncio.Task] = set()
@@ -149,6 +156,24 @@ class WorkerResult(TypedDict):
149
156
  run_ended_at: str | None
150
157
 
151
158
 
159
+ @asynccontextmanager
160
+ async def set_auth_ctx_for_run(run: Run) -> AsyncGenerator[None, None]:
161
+ try:
162
+ user = run["kwargs"]["config"]["configurable"]["langgraph_auth_user"]
163
+ permissions = run["kwargs"]["config"]["configurable"][
164
+ "langgraph_auth_permissions"
165
+ ]
166
+ if user is not None:
167
+ user = normalize_user(user)
168
+ async with with_user(user, permissions):
169
+ yield None
170
+ else:
171
+ yield None
172
+
173
+ except KeyError:
174
+ pass
175
+
176
+
152
177
  async def worker(
153
178
  timeout: float,
154
179
  exit: AsyncExitStack,
@@ -165,7 +190,8 @@ async def worker(
165
190
  webhook = run["kwargs"].pop("webhook", None)
166
191
  run_started_at = datetime.now(UTC)
167
192
  run_ended_at: str | None = None
168
- async with Runs.enter(run_id) as done, exit:
193
+
194
+ async with set_auth_ctx_for_run(run), Runs.enter(run_id) as done, exit:
169
195
  temporary = run["kwargs"].get("temporary", False)
170
196
  run_created_at = run["created_at"].isoformat()
171
197
  await logger.ainfo(
@@ -245,7 +271,20 @@ async def worker(
245
271
  run_ended_at=run_ended_at,
246
272
  run_exec_ms=ms(datetime.now(UTC), run_started_at),
247
273
  )
248
- await Runs.delete(conn, run_id, thread_id=run["thread_id"])
274
+ try:
275
+ await Runs.delete(conn, run_id, thread_id=run["thread_id"])
276
+ except InFailedSqlTransaction as e:
277
+ await logger.ainfo(
278
+ "Ignoring rollback error",
279
+ run_id=str(run_id),
280
+ run_attempt=attempt,
281
+ run_created_at=run_created_at,
282
+ exc=str(e),
283
+ )
284
+ # We need to clean up the transaction early if we want to
285
+ # update the thread status with the same connection
286
+ await exit.aclose()
287
+ checkpoint = None # reset the checkpoint
249
288
  except UserInterrupt as e:
250
289
  exception = e
251
290
  status = "interrupted"
@@ -292,6 +331,7 @@ async def worker(
292
331
  run_exec_ms=ms(datetime.now(UTC), run_started_at),
293
332
  )
294
333
  await Runs.set_status(conn, run_id, "error")
334
+ set_auth_ctx(None, None)
295
335
  # delete or set status of thread
296
336
  if temporary:
297
337
  await Threads.delete(conn, run["thread_id"])
langgraph_api/route.py CHANGED
@@ -14,7 +14,7 @@ from starlette.routing import Route, compile_path, get_name
14
14
  from starlette.types import ASGIApp, Receive, Scope, Send
15
15
 
16
16
  from langgraph_api.serde import json_dumpb
17
- from langgraph_api.utils import set_auth_ctx
17
+ from langgraph_api.utils import get_auth_ctx, with_user
18
18
 
19
19
 
20
20
  def api_request_response(
@@ -116,5 +116,10 @@ class ApiRoute(Route):
116
116
  async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
117
117
  # https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope
118
118
  scope["route"] = self.path
119
- set_auth_ctx(scope.get("user"), scope.get("auth"))
120
- return await super().handle(scope, receive, send)
119
+ ctx = get_auth_ctx()
120
+ if ctx:
121
+ user, auth = ctx.user, ctx.permissions
122
+ else:
123
+ user, auth = scope.get("user"), scope.get("auth")
124
+ async with with_user(user, auth):
125
+ return await super().handle(scope, receive, send)
langgraph_api/schema.py CHANGED
@@ -157,7 +157,7 @@ class RunSend(TypedDict):
157
157
 
158
158
  class RunCommand(TypedDict):
159
159
  goto: str | RunSend | Sequence[RunSend | str] | None
160
- update: dict[str, Any] | None
160
+ update: dict[str, Any] | Sequence[tuple[str, Any]] | None
161
161
  resume: Any | None
162
162
 
163
163
 
langgraph_api/stream.py CHANGED
@@ -75,8 +75,15 @@ def _map_cmd(cmd: RunCommand) -> Command:
75
75
  if goto is not None and not isinstance(goto, list):
76
76
  goto = [cmd.get("goto")]
77
77
 
78
+ update = cmd.get("update")
79
+ if isinstance(update, tuple | list) and all(
80
+ isinstance(t, tuple | list) and len(t) == 2 and isinstance(t[0], str)
81
+ for t in update
82
+ ):
83
+ update = [tuple(t) for t in update]
84
+
78
85
  return Command(
79
- update=cmd.get("update"),
86
+ update=update,
80
87
  goto=(
81
88
  [
82
89
  it if isinstance(it, str) else Send(it["node"], it["input"])
langgraph_api/utils.py CHANGED
@@ -17,22 +17,28 @@ AuthContext = contextvars.ContextVar[Auth.types.BaseAuthContext | None](
17
17
 
18
18
 
19
19
  @asynccontextmanager
20
- async def with_user(user: BaseUser | None = None, auth: AuthCredentials | None = None):
20
+ async def with_user(
21
+ user: BaseUser | None = None, auth: AuthCredentials | list[str] | None = None
22
+ ):
21
23
  current = get_auth_ctx()
22
24
  set_auth_ctx(user, auth)
23
25
  yield
24
26
  if current is None:
25
27
  return
26
- set_auth_ctx(current.user, AuthCredentials(scopes=current.scopes))
28
+ set_auth_ctx(current.user, AuthCredentials(scopes=current.permissions))
27
29
 
28
30
 
29
- def set_auth_ctx(user: BaseUser | None, auth: AuthCredentials | None) -> None:
30
- if not user or not auth:
31
+ def set_auth_ctx(
32
+ user: BaseUser | None, auth: AuthCredentials | list[str] | None
33
+ ) -> None:
34
+ if user is None and auth is None:
31
35
  AuthContext.set(None)
32
36
  else:
33
37
  AuthContext.set(
34
38
  Auth.types.BaseAuthContext(
35
- permissions=auth.scopes,
39
+ permissions=(
40
+ auth.scopes if isinstance(auth, AuthCredentials) else (auth or [])
41
+ ),
36
42
  user=user or SimpleUser(""),
37
43
  )
38
44
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langgraph-api
3
- Version: 0.0.15
3
+ Version: 0.0.16
4
4
  Summary:
5
5
  License: Elastic-2.0
6
6
  Author: Nuno Campos
@@ -16,7 +16,7 @@ Requires-Dist: jsonschema-rs (>=0.25.0,<0.26.0)
16
16
  Requires-Dist: langchain-core (>=0.2.38,<0.4.0)
17
17
  Requires-Dist: langgraph (>=0.2.56,<0.3.0)
18
18
  Requires-Dist: langgraph-checkpoint (>=2.0.7,<3.0)
19
- Requires-Dist: langgraph-sdk (>=0.1.48,<0.2.0)
19
+ Requires-Dist: langgraph-sdk (>=0.1.51,<0.2.0)
20
20
  Requires-Dist: langsmith (>=0.1.63,<0.3.0)
21
21
  Requires-Dist: orjson (>=3.10.1)
22
22
  Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
@@ -5,11 +5,11 @@ langgraph_api/api/assistants.py,sha256=nn-0Q5FTaEbdPq-oesrpVzqu223PDSzeejFy9fd5X
5
5
  langgraph_api/api/meta.py,sha256=hueasWpTDQ6xYLo9Bzt2jhNH8XQRzreH8FTeFfnRoxQ,2700
6
6
  langgraph_api/api/openapi.py,sha256=AUxfnD5hlRp7s-0g2hBC5dNSNk3HTwOLeJiF489DT44,2762
7
7
  langgraph_api/api/runs.py,sha256=wAzPXi_kcYB9BcLBL4FXgkBohWwCPIpe4XERnsnWnsA,16042
8
- langgraph_api/api/store.py,sha256=y7VIejpsE7rpPF-tiMGBqqBwWPZ1wb3o48th6NUvb5I,3849
8
+ langgraph_api/api/store.py,sha256=VzAJVOwO0IxosBB7km5TTf2rhlWGyPkVz_LpvbxetVY,5437
9
9
  langgraph_api/api/threads.py,sha256=taU61XPcCEhBPCYPZcMDsgVDwwWUWJs8p-PrXFXWY48,8661
10
- langgraph_api/asyncio.py,sha256=XiFEllu-Kg4zAO084npHPYOPnLQRire3V75XrVQYMxE,6023
10
+ langgraph_api/asyncio.py,sha256=2fOlx-cZvuj1gQ867Kw1R_wsBsl9jdHYHcUtK2a-x-U,6264
11
11
  langgraph_api/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- langgraph_api/auth/custom.py,sha256=_BlII18X8ji_t8qA9FIUq_ip99dMQGkNscjsuEiLHus,20527
12
+ langgraph_api/auth/custom.py,sha256=g_u7FdKm1Qj8eu_MZdnJeMsI4DlWyU5Mg0rPJzdOTSE,20913
13
13
  langgraph_api/auth/langsmith/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  langgraph_api/auth/langsmith/backend.py,sha256=InScaL-HYCnxYEauhxU198gRZV9pJn9SzzBoR9Edn7g,2654
15
15
  langgraph_api/auth/langsmith/client.py,sha256=eKchvAom7hdkUXauD8vHNceBDDUijrFgdTV8bKd7x4Q,3998
@@ -30,10 +30,10 @@ langgraph_api/js/client.mts,sha256=ksiytm222KTUWj92ZnajqFku_y2AkRmfENmKie5LSPw,2
30
30
  langgraph_api/js/client.new.mts,sha256=9FrArkM20IeD176Q7u5lJNruaQXqAfAdDcqJkF0TPPA,23493
31
31
  langgraph_api/js/errors.py,sha256=Cm1TKWlUCwZReDC5AQ6SgNIVGD27Qov2xcgHyf8-GXo,361
32
32
  langgraph_api/js/global.d.ts,sha256=zR_zLYfpzyPfxpEFth5RgZoyfGulIXyZYPRf7cU0K0Y,106
33
- langgraph_api/js/package.json,sha256=QvZQVTWP0uYnPD_o7ETIuv9Hczn98bWfMEwTgjbBYCs,840
33
+ langgraph_api/js/package.json,sha256=BGnWhiMMvxocEuPciTq14fEHp5NFmHo6fYV8q62n3t4,840
34
34
  langgraph_api/js/remote.py,sha256=D9cqcEgXau-fm_trpNwCHMra5BXntgUa469lgs_a9JQ,622
35
- langgraph_api/js/remote_new.py,sha256=LfCY7DIsJ6DulepYIPmvjIKb3kyDUD9lYrpekXhTF-E,22277
36
- langgraph_api/js/remote_old.py,sha256=mzM-AwYSRZQQTZvbmd680RIIVlSrO9amMtOHq3DQRcI,22088
35
+ langgraph_api/js/remote_new.py,sha256=T_Vr8459bax1C9xxqz_ZYmGivq5Vhspg2Iu9TL0Qc-Q,22707
36
+ langgraph_api/js/remote_old.py,sha256=2a-3ooAYUZs8aPsfnXafbBd4pP7lRmokoU7TiO7P9Js,22546
37
37
  langgraph_api/js/schema.py,sha256=7idnv7URlYUdSNMBXQcw7E4SxaPxCq_Oxwnlml8q5ik,408
38
38
  langgraph_api/js/server_sent_events.py,sha256=DLgXOHauemt7706vnfDUCG1GI3TidKycSizccdz9KgA,3702
39
39
  langgraph_api/js/src/graph.mts,sha256=EO1ITYoKiUykzO_8V8mnQb6NYybooR1VXIovThZzywc,2998
@@ -45,11 +45,11 @@ langgraph_api/js/src/schema/types.template.mts,sha256=c-FA0Ykzp4KvPyYA6a-hDf60Kd
45
45
  langgraph_api/js/src/utils/importMap.mts,sha256=pX4TGOyUpuuWF82kXcxcv3-8mgusRezOGe6Uklm2O5A,1644
46
46
  langgraph_api/js/src/utils/pythonSchemas.mts,sha256=98IW7Z_VP7L_CHNRMb3_MsiV3BgLE2JsWQY_PQcRR3o,685
47
47
  langgraph_api/js/src/utils/serde.mts,sha256=OuyyO9btvwWd55rU_H4x91dFEJiaPxL-lL9O6Zgo908,742
48
- langgraph_api/js/tests/api.test.mts,sha256=sFUW1_ffW3IUsQCdU7ToJ67QiTcwiYlxDJuUVbFh4_I,53215
48
+ langgraph_api/js/tests/api.test.mts,sha256=2EpDEs888pJGdZnsyc76GdMp7uuRxM_SNlHBwITU-5I,55668
49
49
  langgraph_api/js/tests/compose-postgres.yml,sha256=pbNfeqVUqhWILBuUdwAgQOYsVU_fgkCVm0YlTgU8og8,1721
50
50
  langgraph_api/js/tests/graphs/.gitignore,sha256=26J8MarZNXh7snXD5eTpV3CPFTht5Znv8dtHYCLNfkw,12
51
- langgraph_api/js/tests/graphs/agent.mts,sha256=i2s0GOnydW88laDGBatYkQnjUe9Q44RNHDhdEGIcT8w,3811
52
- langgraph_api/js/tests/graphs/delay.mts,sha256=QByzUlICCBhaOBYG1Yi9TSl8Ss6w8sx0lETCCTu0kjY,617
51
+ langgraph_api/js/tests/graphs/agent.mts,sha256=fFHm9vW04UN_2mGcHVHqtFIvPhjyFurBg62DAW-GJd4,3889
52
+ langgraph_api/js/tests/graphs/delay.mts,sha256=CFneKxqI4bGGK0lYjSbe80QirowPQlsRSuhDUKfydhk,703
53
53
  langgraph_api/js/tests/graphs/error.mts,sha256=l4tk89449dj1BnEF_0ZcfPt0Ikk1gl8L1RaSnRfr3xo,487
54
54
  langgraph_api/js/tests/graphs/langgraph.json,sha256=frxd7ZWILdeMYSZgUBH6UO-IR7I2YJSOfOlx2mnO1sI,189
55
55
  langgraph_api/js/tests/graphs/nested.mts,sha256=4G7jSOSaFVQAza-_ARbK-Iai1biLlF2DIPDZXf7PLIY,1245
@@ -58,22 +58,22 @@ langgraph_api/js/tests/graphs/weather.mts,sha256=A7mLK3xW8h5B-ZyJNAyX2M2fJJwzPJz
58
58
  langgraph_api/js/tests/graphs/yarn.lock,sha256=q-1S--E5VWLYtkSv03shqtNzeDDv-N_J-N26FszLsjs,7903
59
59
  langgraph_api/js/tests/parser.test.mts,sha256=3zAbboUNhI-cY3hj4Ssr7J-sQXCBTeeI1ItrkG0Ftuk,26257
60
60
  langgraph_api/js/tests/utils.mts,sha256=2kTybJ3O7Yfe1q3ehDouqV54ibXkNzsPZ_wBZLJvY-4,421
61
- langgraph_api/js/yarn.lock,sha256=OD3NXIjBlUlbb6t8vFhOKlnQm4CvGWhB9Ns9W5omPlo,103690
61
+ langgraph_api/js/yarn.lock,sha256=JtRgt5AXlsD4qBm1Nrtzxg-TLtZkFWtRKCvyS-V8CLg,103690
62
62
  langgraph_api/lifespan.py,sha256=Uj7NV-NqxxD1fgx_umM9pVqclcy-VlqrIxDljyj2he0,1820
63
63
  langgraph_api/logging.py,sha256=tiDNrEFwqaIdL5ywZv908OXlzzfXsPCws9GXeoFtBV8,3367
64
64
  langgraph_api/metadata.py,sha256=mih2G7ScQxiqyUlbksVXkqR3Oo-pM1b6lXtzOsgR1sw,3044
65
65
  langgraph_api/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
- langgraph_api/models/run.py,sha256=qGTcgQjnzmXrxer5VPaGHoGwifV1nyE4ZCXtBiZH1bc,9546
66
+ langgraph_api/models/run.py,sha256=qBN_w8mUpSbAZNVL1683_DfZHQ0zvYMNyqGg-g1sz6Y,9625
67
67
  langgraph_api/patch.py,sha256=94ddcTSZJe22JcpjxiSNjFZdYVnmeoWjk4IX4iBSoyk,1249
68
- langgraph_api/queue.py,sha256=pB2IGzArlmUzKOewJ7ejIOVDPGCYSzG6Mok1gXtsjdA,11955
69
- langgraph_api/route.py,sha256=HnAcWeStCrWNe37YUXIqEsjsrCPCIPGijHG7pM9ga28,4251
70
- langgraph_api/schema.py,sha256=EiCWRR2GmGrBrOYcuK9SeVQS5b98SdaJlKaqOL7t-WQ,5263
68
+ langgraph_api/queue.py,sha256=nYtcjbqjwvELK5OXxD2aw5BWAlSJ-VPyCXSODMMXIj0,13353
69
+ langgraph_api/route.py,sha256=fM4qYCGbmH0a3_cV8uKocb1sLklehxO6HhdRXqLK6OM,4421
70
+ langgraph_api/schema.py,sha256=4aZCFY-dxd_nTot71bdcd9S8QCIgKajuRyj0p2QfgJ4,5291
71
71
  langgraph_api/serde.py,sha256=VoJ7Z1IuqrQGXFzEP1qijAITtWCrmjtVqlCRuScjXJI,3533
72
72
  langgraph_api/server.py,sha256=afHDnL6b_fAIu_q4icnK60a74lHTTZOMIe1egdhRXIk,1522
73
73
  langgraph_api/sse.py,sha256=2wNodCOP2eg7a9mpSu0S3FQ0CHk2BBV_vv0UtIgJIcc,4034
74
74
  langgraph_api/state.py,sha256=8jx4IoTCOjTJuwzuXJKKFwo1VseHjNnw_CCq4x1SW14,2284
75
- langgraph_api/stream.py,sha256=fedsd2rJu9sao6ez4P-_n3Z4jsMzQxxqwssoVr-vbLo,11677
76
- langgraph_api/utils.py,sha256=o7TFlY25IjujeKdXgtyE2mMLPETIlrbOc3w6giYBq2Y,2509
75
+ langgraph_api/stream.py,sha256=Uygx6zcY5Wi9lBhRjtxqVDyLZSF1bsaqxg6mYoYVYcY,11900
76
+ langgraph_api/utils.py,sha256=fMl3DHOQEwAqkFtrnP0Alfbrqw1UvwZ_JVLm-WTSQJk,2654
77
77
  langgraph_api/validation.py,sha256=McizHlz-Ez8Jhdbc79mbPSde7GIuf2Jlbjx2yv_l6dA,4475
78
78
  langgraph_license/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
79
  langgraph_license/middleware.py,sha256=_ODIYzQkymr6W9_Fp9wtf1kAQspnpsmr53xuzyF2GA0,612
@@ -81,15 +81,15 @@ langgraph_license/validation.py,sha256=Uu_G8UGO_WTlLsBEY0gTVWjRR4czYGfw5YAD3HLZo
81
81
  langgraph_storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
82
  langgraph_storage/checkpoint.py,sha256=V4t2GwYEJdPCHbhq_4Udhlv0TWKDzlMu_rlNPdTDc50,3589
83
83
  langgraph_storage/database.py,sha256=Nr5zE9Fur3-tESkqe7xNXMf2QlBuw3H0CUie7jVa6Q4,6003
84
- langgraph_storage/ops.py,sha256=l6sL3wq9vp2OJwhRbdNkPozi-bi2ma-yXHBNGRbmQzk,67985
84
+ langgraph_storage/ops.py,sha256=7kmfm7EO7YbP_ItEjMvFPKPsM0a2X6RMhjaKof63HaQ,68206
85
85
  langgraph_storage/queue.py,sha256=6cTZ0ubHu3S1T43yxHMVOwsQsDaJupByiU0sTUFFls8,3261
86
86
  langgraph_storage/retry.py,sha256=uvYFuXJ-T6S1QY1ZwkZHyZQbsvS-Ab68LSbzbUUSI2E,696
87
87
  langgraph_storage/store.py,sha256=D-p3cWc_umamkKp-6Cz3cAriSACpvM5nxUIvND6PuxE,2710
88
88
  langgraph_storage/ttl_dict.py,sha256=FlpEY8EANeXWKo_G5nmIotPquABZGyIJyk6HD9u6vqY,1533
89
89
  logging.json,sha256=3RNjSADZmDq38eHePMm1CbP6qZ71AmpBtLwCmKU9Zgo,379
90
- openapi.json,sha256=gh6FxpyQqspAuQQH3O22qqGW5owtFj45gyR15QAcS9k,124729
91
- langgraph_api-0.0.15.dist-info/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
92
- langgraph_api-0.0.15.dist-info/METADATA,sha256=Z_TZtWNrQ6jCCYhOHrV3zxhk4Dghxj-YZkyjKu_rqyQ,4041
93
- langgraph_api-0.0.15.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
94
- langgraph_api-0.0.15.dist-info/entry_points.txt,sha256=3EYLgj89DfzqJHHYGxPH4A_fEtClvlRbWRUHaXO7hj4,77
95
- langgraph_api-0.0.15.dist-info/RECORD,,
90
+ openapi.json,sha256=qf2Rw3ieawlAcSJu4mGonh9mNOb6solBD70CGL3w24A,124699
91
+ langgraph_api-0.0.16.dist-info/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
92
+ langgraph_api-0.0.16.dist-info/METADATA,sha256=HLMkdfTmxuTMN59uOzDQ4VCRILZvgslETZShl2Mdruw,4041
93
+ langgraph_api-0.0.16.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
94
+ langgraph_api-0.0.16.dist-info/entry_points.txt,sha256=3EYLgj89DfzqJHHYGxPH4A_fEtClvlRbWRUHaXO7hj4,77
95
+ langgraph_api-0.0.16.dist-info/RECORD,,
langgraph_storage/ops.py CHANGED
@@ -1325,7 +1325,10 @@ class Runs(Authenticated):
1325
1325
  thread = Thread(
1326
1326
  thread_id=thread_id,
1327
1327
  status="busy",
1328
- metadata={"graph_id": assistant["graph_id"]},
1328
+ metadata={
1329
+ "graph_id": assistant["graph_id"],
1330
+ "assistant_id": str(assistant_id),
1331
+ },
1329
1332
  config=Runs._merge_jsonb(
1330
1333
  assistant["config"],
1331
1334
  config,
@@ -1345,7 +1348,11 @@ class Runs(Authenticated):
1345
1348
  if existing_thread["status"] != "busy":
1346
1349
  existing_thread["status"] = "busy"
1347
1350
  existing_thread["metadata"] = Runs._merge_jsonb(
1348
- existing_thread["metadata"], {"graph_id": assistant["graph_id"]}
1351
+ existing_thread["metadata"],
1352
+ {
1353
+ "graph_id": assistant["graph_id"],
1354
+ "assistant_id": str(assistant_id),
1355
+ },
1349
1356
  )
1350
1357
  existing_thread["config"] = Runs._merge_jsonb(
1351
1358
  assistant["config"],
openapi.json CHANGED
@@ -22,7 +22,7 @@
22
22
  "description": "A run is an invocation of a graph / assistant, with no state or memory persistence."
23
23
  },
24
24
  {
25
- "name": "Crons (Enterprise-only)",
25
+ "name": "Crons (Plus tier)",
26
26
  "description": "A cron is a periodic run that recurs on a given schedule. The repeats can be isolated, or share state in a thread"
27
27
  },
28
28
  {
@@ -1473,7 +1473,7 @@
1473
1473
  "/threads/{thread_id}/runs/crons": {
1474
1474
  "post": {
1475
1475
  "tags": [
1476
- "Crons (Enterprise-only)"
1476
+ "Crons (Plus tier)"
1477
1477
  ],
1478
1478
  "summary": "Create Thread Cron",
1479
1479
  "description": "Create a cron to schedule runs on a thread.",
@@ -2058,7 +2058,7 @@
2058
2058
  "/runs/crons": {
2059
2059
  "post": {
2060
2060
  "tags": [
2061
- "Crons (Enterprise-only)"
2061
+ "Crons (Plus tier)"
2062
2062
  ],
2063
2063
  "summary": "Create Cron",
2064
2064
  "description": "Create a cron to schedule runs on new threads.",
@@ -2110,7 +2110,7 @@
2110
2110
  "/runs/crons/search": {
2111
2111
  "post": {
2112
2112
  "tags": [
2113
- "Crons (Enterprise-only)"
2113
+ "Crons (Plus tier)"
2114
2114
  ],
2115
2115
  "summary": "Search Crons",
2116
2116
  "description": "Search all active crons",
@@ -2399,7 +2399,7 @@
2399
2399
  "/runs/crons/{cron_id}": {
2400
2400
  "delete": {
2401
2401
  "tags": [
2402
- "Crons (Enterprise-only)"
2402
+ "Crons (Plus tier)"
2403
2403
  ],
2404
2404
  "summary": "Delete Cron",
2405
2405
  "description": "Delete a cron by ID.",