langgraph-api 0.15.0.dev5__py3-none-any.whl → 0.16.0.dev1__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.
Files changed (30) hide show
  1. langgraph_api/__init__.py +1 -1
  2. langgraph_api/api/a2a.py +56 -12
  3. langgraph_api/api/a2ui.py +18 -8
  4. langgraph_api/api/mcp/_handlers.py +3 -1
  5. langgraph_api/auth/langsmith/backend.py +2 -0
  6. langgraph_api/auth/studio_user.py +6 -1
  7. langgraph_api/config/__init__.py +36 -0
  8. langgraph_api/cron_scheduler.py +1 -1
  9. langgraph_api/encryption/middleware.py +4 -0
  10. langgraph_api/graph.py +14 -2
  11. langgraph_api/js/client.mts +18 -6
  12. langgraph_api/js/package.json +3 -3
  13. langgraph_api/js/yarn.lock +103 -158
  14. langgraph_api/metrics_collector.py +6 -0
  15. langgraph_api/release_tags.py +35 -17
  16. langgraph_api/runner/__init__.py +10 -0
  17. langgraph_api/runner/__main__.py +275 -0
  18. langgraph_api/self_hosted_logs.py +4 -0
  19. langgraph_api/worker.py +36 -163
  20. langgraph_api/worker_runtime.py +221 -0
  21. {langgraph_api-0.15.0.dev5.dist-info → langgraph_api-0.16.0.dev1.dist-info}/METADATA +2 -2
  22. {langgraph_api-0.15.0.dev5.dist-info → langgraph_api-0.16.0.dev1.dist-info}/RECORD +30 -23
  23. langgraph_grpc_common/proto/__init__.py +4 -0
  24. langgraph_grpc_common/proto/runner_pb2.py +50 -0
  25. langgraph_grpc_common/proto/runner_pb2.pyi +167 -0
  26. langgraph_grpc_common/proto/runner_pb2_grpc.py +108 -0
  27. langgraph_grpc_common/proto/runner_pb2_grpc.pyi +73 -0
  28. {langgraph_api-0.15.0.dev5.dist-info → langgraph_api-0.16.0.dev1.dist-info}/WHEEL +0 -0
  29. {langgraph_api-0.15.0.dev5.dist-info → langgraph_api-0.16.0.dev1.dist-info}/entry_points.txt +0 -0
  30. {langgraph_api-0.15.0.dev5.dist-info → langgraph_api-0.16.0.dev1.dist-info}/licenses/LICENSE +0 -0
langgraph_api/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.15.0.dev5"
1
+ __version__ = "0.16.0.dev1"
langgraph_api/api/a2a.py CHANGED
@@ -55,6 +55,8 @@ MAX_HISTORY_LENGTH_REQUESTED = 10
55
55
  LANGGRAPH_HISTORY_QUERY_LIMIT = 500
56
56
  HISTORY_SCOPE_EXTENSION_URI = "https://langchain.com/a2a/extensions/history-scope/v1"
57
57
  HISTORY_SCOPE_HEADER = "LangGraph-A2A-History-Scope"
58
+ A2A_EXTENSIONS_HEADER = "A2A-Extensions"
59
+ A2UI_EXTENSIONS_HEADER = "X-A2A-Extensions"
58
60
  HistoryScope = Literal["context", "task"]
59
61
 
60
62
 
@@ -123,6 +125,7 @@ class A2UIContext:
123
125
  metadata: dict[str, Any]
124
126
  extensions: list[str]
125
127
  drop_invalid: bool = False
128
+ mime_type: str = A2UI_MIME_TYPE
126
129
 
127
130
 
128
131
  _A2UI_DISABLED_CONTEXT = A2UIContext(False, False, {}, [])
@@ -170,6 +173,11 @@ def _a2ui_context_for_run(
170
173
  active=a2a_metadata.get("a2ui_active") is True,
171
174
  metadata={},
172
175
  extensions=[],
176
+ mime_type=(
177
+ a2a_metadata["a2ui_mime_type"]
178
+ if is_a2ui_mime_type(a2a_metadata.get("a2ui_mime_type"))
179
+ else A2UI_MIME_TYPE
180
+ ),
173
181
  )
174
182
 
175
183
 
@@ -197,26 +205,46 @@ def _a2a_run_metadata(
197
205
  **({"task_id": task_id} if task_id else {}),
198
206
  **({"task_start_id": task_start_id} if task_start_id else {}),
199
207
  **({"a2ui_active": context.active} if context.enabled else {}),
208
+ **(
209
+ {"a2ui_mime_type": context.mime_type}
210
+ if context.enabled and context.active
211
+ else {}
212
+ ),
200
213
  }
201
214
  }
202
215
 
203
216
 
204
217
  def _a2ui_message_context(
205
- message: dict[str, Any], assistant: dict[str, Any]
218
+ params: dict[str, Any],
219
+ assistant: dict[str, Any],
220
+ headers: Headers,
221
+ *,
222
+ default_mime_type: str = A2UI_MIME_TYPE,
206
223
  ) -> A2UIContext:
224
+ message = params.get("message") or {}
207
225
  metadata = message.get("metadata") or {}
208
- extensions = message.get("extensions") or []
209
- if (
210
- not isinstance(metadata, dict)
211
- or not isinstance(extensions, list)
212
- or not all(isinstance(extension, str) for extension in extensions)
213
- ):
214
- raise A2UIValidationError("Invalid A2A message metadata or extensions")
226
+ if not isinstance(metadata, dict):
227
+ raise A2UIValidationError("Invalid A2A message metadata")
228
+ extensions: list[str] = []
229
+ for header in (A2A_EXTENSIONS_HEADER, A2UI_EXTENSIONS_HEADER):
230
+ for header_value in headers.getlist(header):
231
+ for value in header_value.split(","):
232
+ if (extension := value.strip()) and extension not in extensions:
233
+ extensions.append(extension)
234
+ mime_type = default_mime_type
235
+ for part in message.get("parts") or []:
236
+ part_metadata = part.get("metadata") if isinstance(part, dict) else None
237
+ if isinstance(part_metadata, dict) and is_a2ui_mime_type(
238
+ part_metadata.get("mimeType")
239
+ ):
240
+ mime_type = part_metadata["mimeType"]
241
+ break
215
242
  return A2UIContext(
216
243
  enabled=_a2ui_enabled(assistant),
217
244
  active=A2UI_EXTENSION_URI in extensions,
218
245
  metadata=metadata,
219
246
  extensions=extensions,
247
+ mime_type=mime_type,
220
248
  )
221
249
 
222
250
 
@@ -892,7 +920,9 @@ def _process_a2a_message_parts(
892
920
  {
893
921
  "type": "data",
894
922
  "data": part_data,
895
- "metadata": normalize_metadata(part_metadata),
923
+ "metadata": normalize_metadata(
924
+ part_metadata, mime_type=a2ui.mime_type
925
+ ),
896
926
  }
897
927
  )
898
928
  continue
@@ -1114,7 +1144,7 @@ def _content_to_a2a_parts(
1114
1144
  {
1115
1145
  "kind": "data",
1116
1146
  "data": data,
1117
- "metadata": normalize_metadata(metadata),
1147
+ "metadata": normalize_metadata(metadata, mime_type=a2ui.mime_type),
1118
1148
  }
1119
1149
  )
1120
1150
  elif (
@@ -2095,6 +2125,7 @@ async def handle_message_send(
2095
2125
  task_id = None
2096
2126
  task_start_id = str(uuid7())
2097
2127
  task_ids_by_start_id: dict[str, str] = {}
2128
+ a2ui_mime_type = A2UI_MIME_TYPE
2098
2129
  else:
2099
2130
  task = await _resolve_task(
2100
2131
  client,
@@ -2121,10 +2152,16 @@ async def handle_message_send(
2121
2152
  task_id = task.task_id
2122
2153
  task_start_id = None
2123
2154
  task_ids_by_start_id = task.task_ids_by_start_id
2155
+ a2ui_mime_type = _a2ui_context_for_run(task.run).mime_type
2124
2156
 
2125
2157
  # Process A2A message parts into LangChain messages format
2126
2158
  try:
2127
- a2ui = _a2ui_message_context(message, assistant)
2159
+ a2ui = _a2ui_message_context(
2160
+ params,
2161
+ assistant,
2162
+ request.headers,
2163
+ default_mime_type=a2ui_mime_type,
2164
+ )
2128
2165
  message_role = _normalize_input_role(message.get("role", "ROLE_USER"))
2129
2166
  input_content = _process_a2a_message_parts(
2130
2167
  parts,
@@ -3352,6 +3389,7 @@ async def handle_message_stream(
3352
3389
  context_id = context_id_from_message or str(uuid.uuid4())
3353
3390
  task_id = None
3354
3391
  task_start_id = str(uuid7())
3392
+ a2ui_mime_type = A2UI_MIME_TYPE
3355
3393
  else:
3356
3394
  task = await _resolve_task(
3357
3395
  client,
@@ -3389,10 +3427,16 @@ async def handle_message_stream(
3389
3427
  context_id = task.context_id
3390
3428
  task_id = task.task_id
3391
3429
  task_start_id = None
3430
+ a2ui_mime_type = _a2ui_context_for_run(task.run).mime_type
3392
3431
 
3393
3432
  # Process A2A message parts into LangChain messages format
3394
3433
  try:
3395
- a2ui = _a2ui_message_context(message, assistant)
3434
+ a2ui = _a2ui_message_context(
3435
+ params,
3436
+ assistant,
3437
+ request.headers,
3438
+ default_mime_type=a2ui_mime_type,
3439
+ )
3396
3440
  message_role = _normalize_input_role(message.get("role", "ROLE_USER"))
3397
3441
  input_content = _process_a2a_message_parts(
3398
3442
  parts,
langgraph_api/api/a2ui.py CHANGED
@@ -9,8 +9,8 @@ import jsonschema_rs
9
9
  import orjson
10
10
 
11
11
  A2UI_EXTENSION_URI = "https://a2ui.org/a2a-extension/a2ui/v0.9"
12
- A2UI_MIME_TYPE = "application/a2ui+json"
13
- _COMPAT_A2UI_MIME_TYPE = "application/json+a2ui"
12
+ A2UI_MIME_TYPE = "application/json+a2ui"
13
+ _COMPAT_A2UI_MIME_TYPE = "application/a2ui+json"
14
14
  _SCHEMA_DIR = Path(__file__).with_name("a2ui_schemas")
15
15
 
16
16
 
@@ -52,9 +52,11 @@ def is_a2ui_mime_type(value: Any) -> bool:
52
52
  return value in {A2UI_MIME_TYPE, _COMPAT_A2UI_MIME_TYPE}
53
53
 
54
54
 
55
- def normalize_metadata(metadata: dict[str, Any]) -> dict[str, Any]:
55
+ def normalize_metadata(
56
+ metadata: dict[str, Any], *, mime_type: str = A2UI_MIME_TYPE
57
+ ) -> dict[str, Any]:
56
58
  if is_a2ui_mime_type(metadata.get("mimeType")):
57
- return {**metadata, "mimeType": A2UI_MIME_TYPE}
59
+ return {**metadata, "mimeType": mime_type}
58
60
  return metadata
59
61
 
60
62
 
@@ -70,9 +72,17 @@ def validate_client_metadata(metadata: dict[str, Any]) -> None:
70
72
  raise A2UIValidationError("Invalid A2UI message metadata") from exc
71
73
 
72
74
 
73
- def validate_payload(data: Any, *, direction: Literal["client", "server"]) -> None:
74
- if not isinstance(data, list):
75
- raise A2UIValidationError("A2UI DataPart data must be an array")
75
+ def validate_payload(
76
+ data: Any,
77
+ *,
78
+ direction: Literal["client", "server"],
79
+ ) -> None:
80
+ if isinstance(data, list):
81
+ messages = data
82
+ elif isinstance(data, dict):
83
+ messages = (data,)
84
+ else:
85
+ raise A2UIValidationError("A2UI DataPart data must be an array or object")
76
86
 
77
87
  validator = (
78
88
  _CLIENT_MESSAGE_VALIDATOR
@@ -80,7 +90,7 @@ def validate_payload(data: Any, *, direction: Literal["client", "server"]) -> No
80
90
  else _SERVER_MESSAGE_VALIDATOR
81
91
  )
82
92
  try:
83
- for message in data:
93
+ for message in messages:
84
94
  validator.validate(message)
85
95
  except ValueError as exc:
86
96
  raise A2UIValidationError(f"Invalid A2UI {direction} message") from exc
@@ -287,7 +287,9 @@ async def handle_tools_list(
287
287
  schemas = await client.assistants.get_schemas(
288
288
  assistant["assistant_id"], headers=request.headers
289
289
  )
290
- input_schema = schemas.get("input_schema") or {}
290
+ input_schema = schemas.get("input_schema", {})
291
+ if input_schema is None:
292
+ raise ValueError("Input schema generation failed")
291
293
  if input_schema:
292
294
  _sanitizers.simplify_mcp_schema_inplace(input_schema)
293
295
  # MCP spec requires inputSchema to be a JSON Schema with type "object"
@@ -21,6 +21,7 @@ class AuthDict(TypedDict):
21
21
  organization_id: str
22
22
  tenant_id: str
23
23
  user_id: NotRequired[str]
24
+ ls_user_id: NotRequired[str]
24
25
  user_email: NotRequired[str]
25
26
 
26
27
 
@@ -94,6 +95,7 @@ class LangsmithAuthBackend(AuthenticationBackend):
94
95
 
95
96
  credentials = AuthCredentials(["authenticated"])
96
97
  user = StudioUser(auth_dict.get("user_id"), is_authenticated=True)
98
+ user.ls_user_id = auth_dict.get("ls_user_id")
97
99
 
98
100
  # Cache the result
99
101
  self._cache.set(cache_key, AuthCacheEntry(credentials=credentials, user=user))
@@ -5,14 +5,19 @@ from starlette.authentication import BaseUser
5
5
  class StudioUser(StudioUserBase, BaseUser):
6
6
  """StudioUser class."""
7
7
 
8
+ ls_user_id: str | None = None
9
+
8
10
  def dict(self):
9
- return {
11
+ user = {
10
12
  "kind": "StudioUser",
11
13
  "is_authenticated": self.is_authenticated,
12
14
  "display_name": self.display_name,
13
15
  "identity": self.identity,
14
16
  "permissions": self.permissions,
15
17
  }
18
+ if self.ls_user_id is not None:
19
+ user["ls_user_id"] = self.ls_user_id
20
+ return user
16
21
 
17
22
  def __getitem__(self, key):
18
23
  return self.dict()[key]
@@ -1,4 +1,5 @@
1
1
  import os
2
+ from enum import StrEnum
2
3
  from os import environ, getenv
3
4
  from typing import TYPE_CHECKING, Annotated, Literal, TypeVar, cast
4
5
 
@@ -398,6 +399,35 @@ if THREAD_TTL is None and CHECKPOINTER_CONFIG is not None:
398
399
  THREAD_TTL = CHECKPOINTER_CONFIG.get("ttl")
399
400
 
400
401
  N_JOBS_PER_WORKER = env("N_JOBS_PER_WORKER", cast=int, default=10)
402
+
403
+
404
+ class DispatchMode(StrEnum):
405
+ """How background runs are dispatched.
406
+
407
+ single-process: Python queue loop in the API process (default).
408
+ multi-process: Go queue worker claims runs and dispatches to Python runners.
409
+ """
410
+
411
+ SINGLE_PROCESS = "single-process"
412
+ MULTI_PROCESS = "multi-process"
413
+
414
+
415
+ def _parse_dispatch_mode(value: str) -> DispatchMode:
416
+ try:
417
+ return DispatchMode(value)
418
+ except ValueError:
419
+ allowed = ", ".join(repr(mode.value) for mode in DispatchMode)
420
+ raise ValueError(
421
+ f"Invalid DISPATCH_MODE={value!r}; expected one of: {allowed}"
422
+ ) from None
423
+
424
+
425
+ # Not in public docs: infrastructure dispatch mode
426
+ DISPATCH_MODE: DispatchMode = env(
427
+ "DISPATCH_MODE",
428
+ cast=_parse_dispatch_mode,
429
+ default=DispatchMode.SINGLE_PROCESS,
430
+ )
401
431
  BG_JOB_TIMEOUT_SECS = env("BG_JOB_TIMEOUT_SECS", cast=float, default=86400)
402
432
  STREAM_PUBLISH_RETRY_MAX_DURATION_SECS = env(
403
433
  "LSD_STREAM_PUBLISH_RETRY_MAX_DURATION_SECS",
@@ -620,7 +650,9 @@ EXPOSE_INTERNAL_METRICS_PROMETHEUS = env(
620
650
 
621
651
  # These are only set for internal agents (polly/insights/engine/etc..) deployed in byoc so we can fetch logs (see api/langgraph_api/self_hosted_logs.py)
622
652
  DATA_PLANE_ID: str | None = env("DATA_PLANE_ID", cast=str, default=None)
653
+ DATA_PLANE_NAME: str | None = env("DATA_PLANE_NAME", cast=str, default=None)
623
654
  DATA_PLANE_ORG_ID: str | None = env("DATA_PLANE_ORG_ID", cast=str, default=None)
655
+ DATA_PLANE_ORG_NAME: str | None = env("DATA_PLANE_ORG_NAME", cast=str, default=None)
624
656
  DATA_PLANE_JWT_SECRET: str | None = env("DATA_PLANE_JWT_SECRET", cast=str, default=None)
625
657
  BYOC_HOST = env("BYOC_HOST", cast=str, default="byoc")
626
658
  BYOC_ENV = env("BYOC_ENV", cast=str, default="byoc")
@@ -687,8 +719,11 @@ __all__ = [
687
719
  "DATADOG_METRICS_ENABLED",
688
720
  "DATA_PLANE_ID",
689
721
  "DATA_PLANE_JWT_SECRET",
722
+ "DATA_PLANE_NAME",
690
723
  "DATA_PLANE_ORG_ID",
724
+ "DATA_PLANE_ORG_NAME",
691
725
  "DB_MIGRATION_BY_CORE_API",
726
+ "DISPATCH_MODE",
692
727
  "EXPOSE_INTERNAL_METRICS_PROMETHEUS",
693
728
  "FF_CRONS_ENABLED",
694
729
  "FF_LOG_DROPPED_EVENTS",
@@ -769,6 +804,7 @@ __all__ = [
769
804
  "AuthConfig",
770
805
  "CheckpointerConfig",
771
806
  "CorsConfig",
807
+ "DispatchMode",
772
808
  "HttpConfig",
773
809
  "SerdeConfig",
774
810
  "StoreConfig",
@@ -15,7 +15,7 @@ from langgraph_api.schema import Cron
15
15
  from langgraph_api.serde import json_loads
16
16
  from langgraph_api.utils import next_cron_date
17
17
  from langgraph_api.utils.config import run_in_executor
18
- from langgraph_api.worker import set_auth_ctx_for_run
18
+ from langgraph_api.worker_runtime import set_auth_ctx_for_run
19
19
  from langgraph_runtime.database import connect
20
20
  from langgraph_runtime.retry import retry_db
21
21
 
@@ -18,6 +18,7 @@ from starlette.middleware.base import BaseHTTPMiddleware
18
18
  from starlette.requests import Request # noqa: TC002
19
19
 
20
20
  from langgraph_api.auth.noop import UnauthenticatedUser
21
+ from langgraph_api.auth.studio_user import StudioUser
21
22
  from langgraph_api.config import LANGGRAPH_ENCRYPTION
22
23
  from langgraph_api.encryption.aes_json import (
23
24
  AesEncryptionInstance,
@@ -74,6 +75,9 @@ def _serialize_user_for_encryption(user: BaseUser) -> dict[str, Any]:
74
75
  if hasattr(user, "model_dump") and callable(user.model_dump):
75
76
  return cast("dict[str, Any]", user.model_dump())
76
77
 
78
+ if isinstance(user, StudioUser):
79
+ return user.dict()
80
+
77
81
  # Plain BaseUser subclasses - extract the required properties
78
82
  return {
79
83
  "identity": user.identity,
langgraph_api/graph.py CHANGED
@@ -502,8 +502,14 @@ def _load_graph_config_from_env() -> dict | None:
502
502
  return config_per_id
503
503
 
504
504
 
505
- async def collect_graphs_from_env(register: bool = False) -> None:
506
- """Return graphs from env."""
505
+ async def collect_graphs_from_env(
506
+ register: bool = False, *, allow_js: bool = True
507
+ ) -> None:
508
+ """Return graphs from env.
509
+
510
+ When ``allow_js`` is False (Python process runner), raise if any JS graph
511
+ specs are configured instead of starting the JS sidecar.
512
+ """
507
513
 
508
514
  # Not in public docs: LANGSERVE_GRAPHS is internal, set by CLI from langgraph.json "graphs" field
509
515
  paths_str = os.getenv("LANGSERVE_GRAPHS")
@@ -595,6 +601,12 @@ async def collect_graphs_from_env(register: bool = False) -> None:
595
601
  js_specs = list(filter(is_js_spec, specs))
596
602
  py_specs = list(filterfalse(is_js_spec, specs))
597
603
 
604
+ if js_specs and not allow_js:
605
+ raise NotImplementedError(
606
+ "JS graphs are not supported in the Python process runner. "
607
+ f"Configured JS graph ids: {[s.id for s in js_specs]}"
608
+ )
609
+
598
610
  if js_specs:
599
611
  if lg_api_config.API_VARIANT == "local_dev":
600
612
  raise NotImplementedError(
@@ -346,7 +346,10 @@ const handleInvoke = <T extends z.ZodType<any>>(
346
346
  const handleStream = <T extends z.ZodType<any>>(
347
347
  name: string,
348
348
  _schema: T,
349
- handler: (rawPayload: z.infer<T>) => AsyncGenerator<any, void, unknown>,
349
+ handler: (
350
+ rawPayload: z.infer<T>,
351
+ signal: AbortSignal,
352
+ ) => AsyncGenerator<any, void, unknown>,
350
353
  ) => {
351
354
  return (c: Context<any, any, { in: z.infer<T>; out: any }>) => {
352
355
  const graphId = c.req.param("graphId");
@@ -370,18 +373,24 @@ const handleStream = <T extends z.ZodType<any>>(
370
373
 
371
374
  try {
372
375
  timer = setTimeout(sendHeartbeat, HEARTBEAT_MS);
373
- for await (const data of handler({ graph_id: graphId, ...body })) {
376
+ for await (const data of handler(
377
+ { graph_id: graphId, ...body },
378
+ c.req.raw.signal,
379
+ )) {
374
380
  await sendSSE(name, data);
375
381
  }
376
382
  } catch (error) {
377
383
  // Still print out the error, as the stack
378
384
  // trace is not carried over in Python
379
385
  logger.error(error);
380
- await sendSSE("error", serializeError(error));
386
+ // The socket is already gone once the request aborts, so skip the frame.
387
+ if (!c.req.raw.signal.aborted) {
388
+ await sendSSE("error", serializeError(error));
389
+ }
390
+ } finally {
391
+ clearTimeout(timer);
392
+ await resolve;
381
393
  }
382
-
383
- clearTimeout(timer);
384
- await resolve;
385
394
  });
386
395
  };
387
396
  };
@@ -682,6 +691,7 @@ function reviveCommand(
682
691
 
683
692
  async function* streamEventsRequest(
684
693
  rawPayload: z.infer<typeof StreamEventsPayload>,
694
+ signal: AbortSignal,
685
695
  ) {
686
696
  const { graph_id: graphId, ...payload } = rawPayload;
687
697
  const config = {
@@ -783,6 +793,7 @@ async function* streamEventsRequest(
783
793
  input,
784
794
  {
785
795
  ...config,
796
+ signal,
786
797
  version: "v3",
787
798
  interruptBefore,
788
799
  interruptAfter,
@@ -803,6 +814,7 @@ async function* streamEventsRequest(
803
814
  input,
804
815
  {
805
816
  ...config,
817
+ signal,
806
818
  version: "v2",
807
819
  streamMode,
808
820
  subgraphs: payload.subgraphs,
@@ -17,9 +17,10 @@
17
17
  "@types/json-schema": "^7.0.15",
18
18
  "dedent": "^1.7.2",
19
19
  "exit-hook": "^5.1.0",
20
- "hono": "^4.12.34",
20
+ "hono": "^4.13.7",
21
21
  "p-queue": "^9.3.1",
22
22
  "p-retry": "^7.1.1",
23
+ "typescript": "^5.9.3",
23
24
  "undici": "^7.28.0",
24
25
  "uuid": "^14.0.1",
25
26
  "winston": "^3.19.0",
@@ -40,9 +41,8 @@
40
41
  "jose": "^6.2.3",
41
42
  "postgres": "^3.4.9",
42
43
  "prettier": "^3.9.5",
43
- "typescript": "^5.9.3",
44
44
  "vite": "^8.1.4",
45
- "vitest": "^4.1.10"
45
+ "vitest": "^5.0.0"
46
46
  },
47
47
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
48
48
  }