langgraph-api 0.18.0.dev10__py3-none-any.whl → 0.18.0.dev12__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.
- langgraph_api/__init__.py +1 -1
- langgraph_api/api/threads.py +15 -18
- langgraph_api/graph.py +4 -1
- langgraph_api/grpc/client.py +8 -0
- langgraph_api/grpc/ops/runs.py +7 -0
- langgraph_api/grpc/ops/threads.py +19 -14
- langgraph_api/js/client.mts +86 -49
- langgraph_api/js/global.d.ts +1 -0
- langgraph_api/js/remote.py +219 -54
- langgraph_api/metadata.py +7 -0
- langgraph_api/models/run.py +5 -8
- langgraph_api/stream.py +36 -32
- langgraph_api/worker.py +9 -0
- {langgraph_api-0.18.0.dev10.dist-info → langgraph_api-0.18.0.dev12.dist-info}/METADATA +2 -2
- {langgraph_api-0.18.0.dev10.dist-info → langgraph_api-0.18.0.dev12.dist-info}/RECORD +20 -20
- langgraph_grpc_common/store.py +9 -2
- openapi.json +30 -4
- {langgraph_api-0.18.0.dev10.dist-info → langgraph_api-0.18.0.dev12.dist-info}/WHEEL +0 -0
- {langgraph_api-0.18.0.dev10.dist-info → langgraph_api-0.18.0.dev12.dist-info}/entry_points.txt +0 -0
- {langgraph_api-0.18.0.dev10.dist-info → langgraph_api-0.18.0.dev12.dist-info}/licenses/LICENSE +0 -0
langgraph_api/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.18.0.
|
|
1
|
+
__version__ = "0.18.0.dev12"
|
langgraph_api/api/threads.py
CHANGED
|
@@ -7,6 +7,7 @@ from starlette.responses import Response
|
|
|
7
7
|
from starlette.routing import BaseRoute
|
|
8
8
|
|
|
9
9
|
from langgraph_api import config as lg_api_config
|
|
10
|
+
from langgraph_api.api.runs import parse_stream_mode_param
|
|
10
11
|
from langgraph_api.config import USE_CUSTOM_CHECKPOINTER
|
|
11
12
|
from langgraph_api.encryption.shared import (
|
|
12
13
|
using_encryption,
|
|
@@ -589,24 +590,20 @@ async def join_thread_stream(request: ApiRequest):
|
|
|
589
590
|
last_event_id, "Invalid last-event-id: must be a valid Redis stream ID"
|
|
590
591
|
)
|
|
591
592
|
|
|
592
|
-
#
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
)
|
|
607
|
-
else:
|
|
608
|
-
# Default to run_modes
|
|
609
|
-
stream_modes = ["run_modes"]
|
|
593
|
+
# The SDKs send ``stream_mode``: once per mode, or as one JSON array from
|
|
594
|
+
# JS SDKs before 1.12.
|
|
595
|
+
params = request.query_params
|
|
596
|
+
values = params.getlist("stream_modes") or params.getlist("stream_mode")
|
|
597
|
+
stream_modes = [
|
|
598
|
+
mode.strip()
|
|
599
|
+
for value in values
|
|
600
|
+
if value
|
|
601
|
+
for item in parse_stream_mode_param(value)
|
|
602
|
+
for mode in str(item).split(",")
|
|
603
|
+
] or ["run_modes"]
|
|
604
|
+
for mode in stream_modes:
|
|
605
|
+
if mode not in get_args(ThreadStreamMode):
|
|
606
|
+
raise HTTPException(status_code=422, detail=f"Invalid stream mode: {mode}")
|
|
610
607
|
|
|
611
608
|
return EventSourceResponse(
|
|
612
609
|
Threads.Stream.join(
|
langgraph_api/graph.py
CHANGED
|
@@ -375,7 +375,10 @@ async def get_graph(
|
|
|
375
375
|
CONFIG_KEY_RUNTIME,
|
|
376
376
|
)
|
|
377
377
|
|
|
378
|
-
if use_langgraph_runtime
|
|
378
|
+
if use_langgraph_runtime or access_context in (
|
|
379
|
+
"threads.read",
|
|
380
|
+
"threads.update",
|
|
381
|
+
):
|
|
379
382
|
# xray draw calls runtime.override(), which the SDK ServerRuntime lacks (LSD-1713).
|
|
380
383
|
from langgraph.runtime import Runtime # noqa: PLC0415
|
|
381
384
|
|
langgraph_api/grpc/client.py
CHANGED
|
@@ -138,6 +138,14 @@ class GrpcClient:
|
|
|
138
138
|
"grpc.http2.initial_window_size",
|
|
139
139
|
config.GRPC_CLIENT_HTTP2_INITIAL_WINDOW_SIZE,
|
|
140
140
|
),
|
|
141
|
+
# The core runs next to the API, so retry a lost connection fast:
|
|
142
|
+
# gRPC's default backoff grows toward two minutes, which kept API
|
|
143
|
+
# pods failing for up to 90 seconds after a core restart that took
|
|
144
|
+
# seconds. Leave grpc.min_reconnect_backoff_ms alone: gRPC uses it
|
|
145
|
+
# as the minimum connect timeout (20 s), not as a floor between
|
|
146
|
+
# retries.
|
|
147
|
+
("grpc.initial_reconnect_backoff_ms", 100),
|
|
148
|
+
("grpc.max_reconnect_backoff_ms", 1000),
|
|
141
149
|
]
|
|
142
150
|
|
|
143
151
|
self._channel = aio.insecure_channel(
|
langgraph_api/grpc/ops/runs.py
CHANGED
|
@@ -1223,6 +1223,13 @@ class Runs(Authenticated):
|
|
|
1223
1223
|
grpc_code=e.code().name,
|
|
1224
1224
|
)
|
|
1225
1225
|
raise
|
|
1226
|
+
except asyncio.CancelledError:
|
|
1227
|
+
logger.info(
|
|
1228
|
+
"Run cancelled, will not signal done",
|
|
1229
|
+
run_id=run_id,
|
|
1230
|
+
thread_id=thread_id,
|
|
1231
|
+
)
|
|
1232
|
+
raise
|
|
1226
1233
|
except BaseException:
|
|
1227
1234
|
logger.exception(
|
|
1228
1235
|
"Non-retriable exception when marking run as done",
|
|
@@ -960,18 +960,21 @@ class Threads(Authenticated):
|
|
|
960
960
|
Yields:
|
|
961
961
|
Tuples of (event_bytes, message_bytes, stream_id_bytes|None)
|
|
962
962
|
"""
|
|
963
|
-
async
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
963
|
+
async with aclosing(
|
|
964
|
+
Threads.Stream.join_event_streaming(
|
|
965
|
+
thread_id,
|
|
966
|
+
stream_modes=stream_modes,
|
|
967
|
+
last_event_id=last_event_id,
|
|
968
|
+
ctx=ctx,
|
|
969
|
+
)
|
|
970
|
+
) as events:
|
|
971
|
+
async for (
|
|
972
|
+
event_bytes,
|
|
973
|
+
message_bytes,
|
|
974
|
+
stream_id_bytes,
|
|
975
|
+
_run_id,
|
|
976
|
+
) in events:
|
|
977
|
+
yield (event_bytes, message_bytes, stream_id_bytes)
|
|
975
978
|
|
|
976
979
|
@staticmethod
|
|
977
980
|
async def join_event_streaming(
|
|
@@ -1006,9 +1009,9 @@ class Threads(Authenticated):
|
|
|
1006
1009
|
request.last_event_id = last_event_id
|
|
1007
1010
|
|
|
1008
1011
|
client = await get_shared_client()
|
|
1009
|
-
|
|
1012
|
+
call = client.threads.Stream(request)
|
|
1010
1013
|
try:
|
|
1011
|
-
async for event in
|
|
1014
|
+
async for event in call:
|
|
1012
1015
|
# Convert protobuf StreamEvent to tuple format
|
|
1013
1016
|
event_bytes = event.event_type.encode("utf-8")
|
|
1014
1017
|
message_bytes = event.message
|
|
@@ -1028,6 +1031,8 @@ class Threads(Authenticated):
|
|
|
1028
1031
|
if isinstance(e, AioRpcError):
|
|
1029
1032
|
_handle_grpc_error(e)
|
|
1030
1033
|
raise
|
|
1034
|
+
finally:
|
|
1035
|
+
call.cancel()
|
|
1031
1036
|
|
|
1032
1037
|
@staticmethod
|
|
1033
1038
|
async def check_thread_stream_auth(
|
langgraph_api/js/client.mts
CHANGED
|
@@ -6,7 +6,7 @@ import { serve } from "@hono/node-server";
|
|
|
6
6
|
import { zValidator } from "@hono/zod-validator";
|
|
7
7
|
import { streamSSE, stream } from "hono/streaming";
|
|
8
8
|
import { HTTPException } from "hono/http-exception";
|
|
9
|
-
import { fetch } from "undici";
|
|
9
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
10
10
|
import pRetry from "p-retry";
|
|
11
11
|
import {
|
|
12
12
|
getConfig,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type ChannelVersions,
|
|
27
27
|
type ChannelProtocol,
|
|
28
28
|
} from "@langchain/langgraph-checkpoint";
|
|
29
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
29
30
|
import * as fs from "node:fs/promises";
|
|
30
31
|
import * as path from "node:path";
|
|
31
32
|
import { serialiseAsDict, serializeError } from "./src/utils/serde.mjs";
|
|
@@ -227,6 +228,44 @@ async function getOrExtractSchema(graphId: string) {
|
|
|
227
228
|
// Pinned by the Python parent (see remote.py); base ports are the fallback.
|
|
228
229
|
const GRAPH_PORT = Number(process.env.LANGGRAPH_JS_GRAPH_PORT) || 5556;
|
|
229
230
|
const REMOTE_PORT = Number(process.env.LANGGRAPH_JS_REMOTE_PORT) || 5555;
|
|
231
|
+
const REMOTE_SOCKET = process.env.LANGGRAPH_JS_REMOTE_SOCKET;
|
|
232
|
+
|
|
233
|
+
// Unix: HTTP over a Unix socket (dummy host; undici dials socketPath).
|
|
234
|
+
// Windows: TCP localhost for checkpointer/store only; no loopback client.
|
|
235
|
+
const remoteAgent = REMOTE_SOCKET
|
|
236
|
+
? new Agent({ connect: { socketPath: REMOTE_SOCKET } })
|
|
237
|
+
: undefined;
|
|
238
|
+
const REMOTE_ORIGIN = REMOTE_SOCKET
|
|
239
|
+
? "http://loopback"
|
|
240
|
+
: `http://localhost:${REMOTE_PORT}`;
|
|
241
|
+
|
|
242
|
+
const LOOPBACK_RUN_HEADER = "x-langgraph-loopback-run";
|
|
243
|
+
const loopbackRun = new AsyncLocalStorage<string>();
|
|
244
|
+
|
|
245
|
+
function remoteFetch(
|
|
246
|
+
input: Parameters<typeof undiciFetch>[0],
|
|
247
|
+
init?: Parameters<typeof undiciFetch>[1],
|
|
248
|
+
) {
|
|
249
|
+
const token = loopbackRun.getStore();
|
|
250
|
+
if (token) {
|
|
251
|
+
const headers = new Headers(init?.headers as HeadersInit | undefined);
|
|
252
|
+
headers.set(LOOPBACK_RUN_HEADER, token);
|
|
253
|
+
init = { ...init, headers };
|
|
254
|
+
}
|
|
255
|
+
if (remoteAgent) {
|
|
256
|
+
return undiciFetch(input, { ...init, dispatcher: remoteAgent });
|
|
257
|
+
}
|
|
258
|
+
return undiciFetch(input, init);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Default `new Client()` (no apiUrl) reads these symbols.
|
|
262
|
+
const LOOPBACK_URL = REMOTE_SOCKET ? `${REMOTE_ORIGIN}/loopback` : undefined;
|
|
263
|
+
|
|
264
|
+
if (LOOPBACK_URL) {
|
|
265
|
+
const g = globalThis as unknown as Record<symbol, unknown>;
|
|
266
|
+
g[Symbol.for("langgraph_api:url")] = LOOPBACK_URL;
|
|
267
|
+
if (remoteAgent) g[Symbol.for("langgraph_api:fetch")] = remoteFetch;
|
|
268
|
+
}
|
|
230
269
|
|
|
231
270
|
const RunnableConfigSchema = z.object({
|
|
232
271
|
tags: z.array(z.string()).optional(),
|
|
@@ -253,10 +292,10 @@ const getRunnableConfig = (
|
|
|
253
292
|
};
|
|
254
293
|
};
|
|
255
294
|
|
|
256
|
-
function tryFetch(...args: Parameters<typeof
|
|
295
|
+
function tryFetch(...args: Parameters<typeof remoteFetch>) {
|
|
257
296
|
return pRetry(
|
|
258
297
|
async () => {
|
|
259
|
-
const response = await
|
|
298
|
+
const response = await remoteFetch(...args).catch((error) => {
|
|
260
299
|
throw new Error(`${args[0]} connecfailed: ${error}`);
|
|
261
300
|
});
|
|
262
301
|
|
|
@@ -264,7 +303,7 @@ function tryFetch(...args: Parameters<typeof fetch>) {
|
|
|
264
303
|
let errorMessage = `${args[0]} failed: HTTP ${response.status}`;
|
|
265
304
|
try {
|
|
266
305
|
errorMessage += `: ${await response.text()}`;
|
|
267
|
-
} catch {
|
|
306
|
+
} catch {}
|
|
268
307
|
throw new Error(errorMessage);
|
|
269
308
|
}
|
|
270
309
|
|
|
@@ -283,7 +322,7 @@ async function sendRecv<T = any>(
|
|
|
283
322
|
method: `${"checkpointer" | "store"}_${string}`,
|
|
284
323
|
data: unknown,
|
|
285
324
|
): Promise<T> {
|
|
286
|
-
const res = await tryFetch(
|
|
325
|
+
const res = await tryFetch(`${REMOTE_ORIGIN}/${method}`, {
|
|
287
326
|
method: "POST",
|
|
288
327
|
headers: { "Content-Type": "application/json" },
|
|
289
328
|
body: JSON.stringify(data),
|
|
@@ -655,9 +694,7 @@ const StreamEventsPayload = z.object({
|
|
|
655
694
|
.nullish(),
|
|
656
695
|
})
|
|
657
696
|
.nullish(),
|
|
658
|
-
stream_mode: z
|
|
659
|
-
.union([z.string(), z.array(z.string())])
|
|
660
|
-
.optional(),
|
|
697
|
+
stream_mode: z.union([z.string(), z.array(z.string())]).optional(),
|
|
661
698
|
config: RunnableConfigSchema.nullish(),
|
|
662
699
|
context: z.record(z.unknown()).nullish(),
|
|
663
700
|
interrupt_before: z.union([z.array(z.string()), z.literal("*")]).nullish(),
|
|
@@ -671,6 +708,7 @@ const StreamEventsPayload = z.object({
|
|
|
671
708
|
// streaming endpoints.
|
|
672
709
|
stream_protocol: z.enum(["v2", "v3"]).nullish(),
|
|
673
710
|
output_keys: z.union([z.string(), z.array(z.string())]).nullish(),
|
|
711
|
+
loopback_token: z.string().nullish(),
|
|
674
712
|
});
|
|
675
713
|
|
|
676
714
|
function reviveCommand(
|
|
@@ -697,7 +735,8 @@ async function* streamEventsRequest(
|
|
|
697
735
|
rawPayload: z.infer<typeof StreamEventsPayload>,
|
|
698
736
|
signal: AbortSignal,
|
|
699
737
|
) {
|
|
700
|
-
const { graph_id: graphId, ...payload } = rawPayload;
|
|
738
|
+
const { graph_id: graphId, loopback_token: token, ...payload } = rawPayload;
|
|
739
|
+
if (token) loopbackRun.enterWith(token);
|
|
701
740
|
const config = {
|
|
702
741
|
...getRunnableConfig(payload.config),
|
|
703
742
|
context: payload.context ?? undefined,
|
|
@@ -793,47 +832,41 @@ async function* streamEventsRequest(
|
|
|
793
832
|
options: Record<string, unknown>,
|
|
794
833
|
) => Promise<AsyncIterable<unknown>>;
|
|
795
834
|
}
|
|
796
|
-
).streamEvents(
|
|
797
|
-
input,
|
|
798
|
-
{
|
|
799
|
-
...config,
|
|
800
|
-
signal,
|
|
801
|
-
version: "v3",
|
|
802
|
-
outputKeys:
|
|
803
|
-
payload.output_keys === undefined
|
|
804
|
-
? graph.outputChannels
|
|
805
|
-
: (payload.output_keys ?? undefined),
|
|
806
|
-
interruptBefore,
|
|
807
|
-
interruptAfter,
|
|
808
|
-
...(callbacks ? { callbacks } : {}),
|
|
809
|
-
...(payload.durability === undefined
|
|
810
|
-
? {}
|
|
811
|
-
: { durability: payload.durability }),
|
|
812
|
-
},
|
|
813
|
-
);
|
|
814
|
-
|
|
815
|
-
for await (const event of protocolStream) {
|
|
816
|
-
yield event;
|
|
817
|
-
}
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
for await (const data of graph.streamEvents(
|
|
822
|
-
input,
|
|
823
|
-
{
|
|
835
|
+
).streamEvents(input, {
|
|
824
836
|
...config,
|
|
825
837
|
signal,
|
|
826
|
-
version: "
|
|
827
|
-
|
|
828
|
-
|
|
838
|
+
version: "v3",
|
|
839
|
+
outputKeys:
|
|
840
|
+
payload.output_keys === undefined
|
|
841
|
+
? graph.outputChannels
|
|
842
|
+
: (payload.output_keys ?? undefined),
|
|
829
843
|
interruptBefore,
|
|
830
844
|
interruptAfter,
|
|
831
845
|
...(callbacks ? { callbacks } : {}),
|
|
832
846
|
...(payload.durability === undefined
|
|
833
847
|
? {}
|
|
834
848
|
: { durability: payload.durability }),
|
|
835
|
-
}
|
|
836
|
-
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
for await (const event of protocolStream) {
|
|
852
|
+
yield event;
|
|
853
|
+
}
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
for await (const data of graph.streamEvents(input, {
|
|
858
|
+
...config,
|
|
859
|
+
signal,
|
|
860
|
+
version: "v2",
|
|
861
|
+
streamMode,
|
|
862
|
+
subgraphs: payload.subgraphs,
|
|
863
|
+
interruptBefore,
|
|
864
|
+
interruptAfter,
|
|
865
|
+
...(callbacks ? { callbacks } : {}),
|
|
866
|
+
...(payload.durability === undefined
|
|
867
|
+
? {}
|
|
868
|
+
: { durability: payload.durability }),
|
|
869
|
+
})) {
|
|
837
870
|
// TODO: upstream this fix to LangGraphJS
|
|
838
871
|
if (streamMode.length === 1 && !Array.isArray(data.data.chunk)) {
|
|
839
872
|
data.data.chunk = [streamMode[0], data.data.chunk];
|
|
@@ -850,7 +883,11 @@ async function* streamEventsRequest(
|
|
|
850
883
|
if (userStreamMode.includes("messages")) {
|
|
851
884
|
if (data.event === "on_chain_stream" && data.run_id === config.runId) {
|
|
852
885
|
const newMessages: Array<BaseMessageChunk> = [];
|
|
853
|
-
const
|
|
886
|
+
const tuple: unknown[] = data.data.chunk;
|
|
887
|
+
// With subgraphs on, chunks are [ns, mode, chunk]; a subgraph's
|
|
888
|
+
// values are not the root graph's messages.
|
|
889
|
+
if (tuple.length === 3 && (tuple[0] as string[]).length > 0) continue;
|
|
890
|
+
const chunk: any = tuple.at(-1);
|
|
854
891
|
|
|
855
892
|
let chunkMessages: Array<BaseMessageChunk> = [];
|
|
856
893
|
if (
|
|
@@ -1140,16 +1177,16 @@ async function main() {
|
|
|
1140
1177
|
meta.threshold_ms = GRAPH_ERROR_MS;
|
|
1141
1178
|
logger.error(
|
|
1142
1179
|
"Graph import exceeded error threshold. " +
|
|
1143
|
-
|
|
1144
|
-
|
|
1180
|
+
"Slow initialization (often due to work executed at import time) can delay readiness, " +
|
|
1181
|
+
"slow scale-out velocity, and may cause deployments to be marked unhealthy.",
|
|
1145
1182
|
meta,
|
|
1146
1183
|
);
|
|
1147
1184
|
} else if (durationMs > GRAPH_WARN_MS) {
|
|
1148
1185
|
meta.threshold_ms = GRAPH_WARN_MS;
|
|
1149
1186
|
logger.warn(
|
|
1150
1187
|
"Graph import exceeded warn threshold. " +
|
|
1151
|
-
|
|
1152
|
-
|
|
1188
|
+
"Slow initialization (often due to work executed at import time) can delay readiness, " +
|
|
1189
|
+
"slow scale-out velocity, and may cause deployments to be marked unhealthy.",
|
|
1153
1190
|
meta,
|
|
1154
1191
|
);
|
|
1155
1192
|
} else {
|
|
@@ -1172,14 +1209,14 @@ async function main() {
|
|
|
1172
1209
|
totalMeta.threshold_ms = GRAPH_ERROR_MS;
|
|
1173
1210
|
logger.error(
|
|
1174
1211
|
"All graphs resolved but exceeded error threshold. " +
|
|
1175
|
-
|
|
1212
|
+
"Consider reducing the number of graphs or optimizing their imports.",
|
|
1176
1213
|
totalMeta,
|
|
1177
1214
|
);
|
|
1178
1215
|
} else if (resolveTotal > GRAPH_WARN_MS) {
|
|
1179
1216
|
totalMeta.threshold_ms = GRAPH_WARN_MS;
|
|
1180
1217
|
logger.warn(
|
|
1181
1218
|
"All graphs resolved but exceeded warn threshold. " +
|
|
1182
|
-
|
|
1219
|
+
"Consider optimizing graph imports to improve startup time.",
|
|
1183
1220
|
totalMeta,
|
|
1184
1221
|
);
|
|
1185
1222
|
} else {
|
langgraph_api/js/global.d.ts
CHANGED
langgraph_api/js/remote.py
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import contextlib
|
|
3
|
+
import copy
|
|
3
4
|
import json
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
8
|
+
import secrets
|
|
7
9
|
import shutil
|
|
8
10
|
import ssl
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import time
|
|
9
14
|
from collections import deque
|
|
10
|
-
from collections.abc import AsyncIterator, Callable
|
|
15
|
+
from collections.abc import AsyncIterator, Callable, Iterator, Mapping
|
|
11
16
|
from inspect import signature
|
|
12
17
|
from typing import Any, Literal, Self, cast
|
|
13
18
|
|
|
@@ -24,6 +29,7 @@ from langchain_core.runnables.schema import (
|
|
|
24
29
|
StandardStreamEvent,
|
|
25
30
|
StreamEvent,
|
|
26
31
|
)
|
|
32
|
+
from langgraph.checkpoint.memory import InMemorySaver
|
|
27
33
|
from langgraph.checkpoint.serde.base import SerializerProtocol
|
|
28
34
|
from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
|
|
29
35
|
from langgraph.types import Command, PregelTask, Send, StateSnapshot
|
|
@@ -52,13 +58,17 @@ from langgraph_api.js.sse import SSEDecoder, aiter_lines_raw
|
|
|
52
58
|
from langgraph_api.route import ApiResponse
|
|
53
59
|
from langgraph_api.schema import Config
|
|
54
60
|
from langgraph_api.serde import json_dumpb
|
|
55
|
-
from langgraph_api.utils import get_auth_ctx, get_user_id
|
|
61
|
+
from langgraph_api.utils import AuthContext, get_auth_ctx, get_user_id
|
|
56
62
|
|
|
57
63
|
logger = structlog.stdlib.get_logger(__name__)
|
|
58
64
|
|
|
59
|
-
# Loopback
|
|
60
|
-
#
|
|
61
|
-
#
|
|
65
|
+
# Loopback IPC between the Python parent and its JS subprocess.
|
|
66
|
+
# GRAPH_* stay on TCP; the remote server (checkpointer/store + /loopback) is a
|
|
67
|
+
# Unix socket except on Windows, where uvicorn cannot bind AF_UNIX.
|
|
68
|
+
# Containers colocated in one pod share a network namespace, so each
|
|
69
|
+
# entrypoint gets its own TCP range (via the offset below); LANGGRAPH_JS_*_PORT
|
|
70
|
+
# overrides any port. The remote socket path includes the pid so API and queue
|
|
71
|
+
# workers in one container do not collide.
|
|
62
72
|
_BASE_REMOTE_PORT = 5555
|
|
63
73
|
_BASE_GRAPH_PORT = 5556
|
|
64
74
|
_BASE_GRAPH_HTTP_PORT = 5557
|
|
@@ -90,6 +100,11 @@ GRAPH_PORT = _resolve_js_port(
|
|
|
90
100
|
GRAPH_HTTP_PORT = _resolve_js_port(
|
|
91
101
|
_BASE_GRAPH_HTTP_PORT, _JS_PORT_OFFSET, os.getenv("LANGGRAPH_JS_GRAPH_HTTP_PORT")
|
|
92
102
|
)
|
|
103
|
+
REMOTE_SOCKET = (
|
|
104
|
+
None
|
|
105
|
+
if sys.platform == "win32"
|
|
106
|
+
else os.path.join(tempfile.gettempdir(), f"lg-js-r{os.getpid()}.sock")
|
|
107
|
+
)
|
|
93
108
|
SSL = ssl.create_default_context(cafile=certifi.where())
|
|
94
109
|
|
|
95
110
|
if (port := int(os.getenv("PORT", "8080"))) and port in (GRAPH_PORT, REMOTE_PORT):
|
|
@@ -97,6 +112,40 @@ if (port := int(os.getenv("PORT", "8080"))) and port in (GRAPH_PORT, REMOTE_PORT
|
|
|
97
112
|
f"PORT={port} is a reserved port for the JS worker. Please choose a different port."
|
|
98
113
|
)
|
|
99
114
|
|
|
115
|
+
|
|
116
|
+
def _js_graph_worker_env(**extra: str | None) -> dict[str, str]:
|
|
117
|
+
"""Env for the Node graph worker. Unix only: path of the remote UDS."""
|
|
118
|
+
env = {
|
|
119
|
+
"LANGCHAIN_CALLBACKS_BACKGROUND": "true",
|
|
120
|
+
"CHOKIDAR_USEPOLLING": "true",
|
|
121
|
+
**os.environ,
|
|
122
|
+
"LANGGRAPH_JS_REMOTE_PORT": str(REMOTE_PORT),
|
|
123
|
+
"LANGGRAPH_JS_GRAPH_PORT": str(GRAPH_PORT),
|
|
124
|
+
"LANGGRAPH_JS_GRAPH_HTTP_PORT": str(GRAPH_HTTP_PORT),
|
|
125
|
+
**{key: value for key, value in extra.items() if value is not None},
|
|
126
|
+
}
|
|
127
|
+
if REMOTE_SOCKET:
|
|
128
|
+
env["LANGGRAPH_JS_REMOTE_SOCKET"] = REMOTE_SOCKET
|
|
129
|
+
else:
|
|
130
|
+
env.pop("LANGGRAPH_JS_REMOTE_SOCKET", None)
|
|
131
|
+
return env
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _checkpointer_httpx_client() -> httpx.AsyncClient:
|
|
135
|
+
"""Client for the remote server: Unix socket, or TCP localhost on Windows."""
|
|
136
|
+
if REMOTE_SOCKET:
|
|
137
|
+
return httpx.AsyncClient(
|
|
138
|
+
base_url="http://loopback",
|
|
139
|
+
limits=httpx.Limits(max_connections=1),
|
|
140
|
+
transport=httpx.AsyncHTTPTransport(uds=REMOTE_SOCKET, verify=SSL),
|
|
141
|
+
)
|
|
142
|
+
return httpx.AsyncClient(
|
|
143
|
+
base_url=f"http://localhost:{REMOTE_PORT}",
|
|
144
|
+
limits=httpx.Limits(max_connections=1),
|
|
145
|
+
transport=httpx.AsyncHTTPTransport(verify=SSL),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
100
149
|
# Per-graph context schema, prefetched at startup so get_context_jsonschema stays sync.
|
|
101
150
|
_context_jsonschema_cache: dict[str, dict[str, Any] | None] = {}
|
|
102
151
|
|
|
@@ -259,11 +308,14 @@ class RemotePregel(BaseRemotePregel):
|
|
|
259
308
|
**kwargs,
|
|
260
309
|
}
|
|
261
310
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
311
|
+
with loopback_grant() as token:
|
|
312
|
+
async for event in _client_stream(
|
|
313
|
+
"streamEvents", {**data, "loopback_token": token}
|
|
314
|
+
):
|
|
315
|
+
if event["event"] == "on_custom_event":
|
|
316
|
+
yield CustomStreamEvent(**event)
|
|
317
|
+
else:
|
|
318
|
+
yield StandardStreamEvent(**event)
|
|
267
319
|
|
|
268
320
|
async def astream_protocol_events(
|
|
269
321
|
self,
|
|
@@ -292,8 +344,11 @@ class RemotePregel(BaseRemotePregel):
|
|
|
292
344
|
**kwargs,
|
|
293
345
|
}
|
|
294
346
|
|
|
295
|
-
|
|
296
|
-
|
|
347
|
+
with loopback_grant() as token:
|
|
348
|
+
async for event in _client_stream(
|
|
349
|
+
"streamEvents", {**data, "loopback_token": token}
|
|
350
|
+
):
|
|
351
|
+
yield event
|
|
297
352
|
|
|
298
353
|
async def fetch_state_schema(self):
|
|
299
354
|
return await _client_invoke(
|
|
@@ -540,6 +595,19 @@ class RemotePregel(BaseRemotePregel):
|
|
|
540
595
|
return config
|
|
541
596
|
|
|
542
597
|
|
|
598
|
+
# A crash loop is this many JS process exits within JS_PROCESS_CRASH_LOOP_SECS,
|
|
599
|
+
# and takes the API down. Exits further apart, even many over the server's
|
|
600
|
+
# life, restart the process.
|
|
601
|
+
JS_PROCESS_CRASH_LOOP_EXITS = 4
|
|
602
|
+
JS_PROCESS_CRASH_LOOP_SECS = 600
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _is_crash_loop(exits: deque[float]) -> bool:
|
|
606
|
+
return (
|
|
607
|
+
len(exits) == exits.maxlen and exits[-1] - exits[0] < JS_PROCESS_CRASH_LOOP_SECS
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
|
|
543
611
|
async def run_js_process(paths_str: str | None, watch: bool = False):
|
|
544
612
|
# check if tsx is available
|
|
545
613
|
tsx_path = shutil.which("tsx")
|
|
@@ -548,6 +616,7 @@ async def run_js_process(paths_str: str | None, watch: bool = False):
|
|
|
548
616
|
"tsx not found in PATH. Please upgrade to latest LangGraph CLI to support running JS graphs."
|
|
549
617
|
)
|
|
550
618
|
attempt = 0
|
|
619
|
+
exits: deque[float] = deque(maxlen=JS_PROCESS_CRASH_LOOP_EXITS)
|
|
551
620
|
while (current_task := asyncio.current_task()) and not current_task.cancelled():
|
|
552
621
|
client_file = os.path.join(os.path.dirname(__file__), "client.mts")
|
|
553
622
|
client_preload_file = os.path.join(
|
|
@@ -570,17 +639,10 @@ async def run_js_process(paths_str: str | None, watch: bool = False):
|
|
|
570
639
|
try:
|
|
571
640
|
process = await asyncio.create_subprocess_exec(
|
|
572
641
|
*args,
|
|
573
|
-
env=
|
|
574
|
-
|
|
575
|
-
"
|
|
576
|
-
|
|
577
|
-
"CHOKIDAR_USEPOLLING": "true",
|
|
578
|
-
**os.environ,
|
|
579
|
-
# Pin the child to the parent's resolved ports.
|
|
580
|
-
"LANGGRAPH_JS_REMOTE_PORT": str(REMOTE_PORT),
|
|
581
|
-
"LANGGRAPH_JS_GRAPH_PORT": str(GRAPH_PORT),
|
|
582
|
-
"LANGGRAPH_JS_GRAPH_HTTP_PORT": str(GRAPH_HTTP_PORT),
|
|
583
|
-
},
|
|
642
|
+
env=_js_graph_worker_env(
|
|
643
|
+
LANGSERVE_GRAPHS=paths_str,
|
|
644
|
+
NODE_ENV="development" if watch else "production",
|
|
645
|
+
),
|
|
584
646
|
)
|
|
585
647
|
logger.info(
|
|
586
648
|
"Started JS graphs process",
|
|
@@ -604,11 +666,11 @@ async def run_js_process(paths_str: str | None, watch: bool = False):
|
|
|
604
666
|
await process.wait()
|
|
605
667
|
raise
|
|
606
668
|
except Exception:
|
|
607
|
-
|
|
669
|
+
exits.append(time.monotonic())
|
|
670
|
+
if _is_crash_loop(exits):
|
|
608
671
|
raise
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
attempt += 1
|
|
672
|
+
logger.warning("Restarting JS process", recent_exits=len(exits))
|
|
673
|
+
attempt += 1
|
|
612
674
|
|
|
613
675
|
|
|
614
676
|
async def run_js_http_process(
|
|
@@ -622,6 +684,7 @@ async def run_js_http_process(
|
|
|
622
684
|
)
|
|
623
685
|
|
|
624
686
|
attempt = 0
|
|
687
|
+
exits: deque[float] = deque(maxlen=JS_PROCESS_CRASH_LOOP_EXITS)
|
|
625
688
|
while (current_task := asyncio.current_task()) and not current_task.cancelled():
|
|
626
689
|
client_file = os.path.join(os.path.dirname(__file__), "client.http.mts")
|
|
627
690
|
args = ("tsx", "watch", client_file) if watch else ("tsx", client_file)
|
|
@@ -669,12 +732,12 @@ async def run_js_http_process(
|
|
|
669
732
|
await process.wait()
|
|
670
733
|
raise
|
|
671
734
|
except Exception:
|
|
672
|
-
|
|
735
|
+
exits.append(time.monotonic())
|
|
736
|
+
if _is_crash_loop(exits):
|
|
673
737
|
logger.exception("JS HTTP process failed")
|
|
674
738
|
raise
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
attempt += 1
|
|
739
|
+
logger.warning("Restarting JS HTTP process", recent_exits=len(exits))
|
|
740
|
+
attempt += 1
|
|
678
741
|
|
|
679
742
|
|
|
680
743
|
_BAD_SURROGATE_RE = re.compile(r"\\u[dD][89a-fA-F][0-9a-fA-F]{2}")
|
|
@@ -718,6 +781,11 @@ async def _get_passthrough_checkpointer():
|
|
|
718
781
|
from langgraph_api import _checkpointer as api_checkpointer # noqa: PLC0415
|
|
719
782
|
|
|
720
783
|
checkpointer = await api_checkpointer.get_checkpointer()
|
|
784
|
+
# The in-memory runtime returns the saver Python graphs share, so swap the
|
|
785
|
+
# serde on a copy; the copy still shares the saved data. Other runtimes
|
|
786
|
+
# return a fresh saver per call.
|
|
787
|
+
if isinstance(checkpointer, InMemorySaver):
|
|
788
|
+
checkpointer = copy.copy(checkpointer)
|
|
721
789
|
# This checkpointer does not attempt to revive LC-objects.
|
|
722
790
|
# Instead, it will pass through the JSON values as-is.
|
|
723
791
|
checkpointer.serde = PassthroughSerialiser()
|
|
@@ -975,20 +1043,125 @@ async def run_remote_checkpointer():
|
|
|
975
1043
|
]
|
|
976
1044
|
)
|
|
977
1045
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1046
|
+
log_kwargs = {
|
|
1047
|
+
# We need to _explicitly_ set these values in order
|
|
1048
|
+
# to avoid reinitialising the logger, which removes
|
|
1049
|
+
# the structlog logger setup before.
|
|
1050
|
+
# See: https://github.com/encode/uvicorn/blob/8f4c8a7f34914c16650ebd026127b96560425fde/uvicorn/config.py#L357-L393
|
|
1051
|
+
"log_config": None,
|
|
1052
|
+
"log_level": None,
|
|
1053
|
+
"access_log": True,
|
|
1054
|
+
}
|
|
1055
|
+
if REMOTE_SOCKET:
|
|
1056
|
+
with contextlib.suppress(FileNotFoundError):
|
|
1057
|
+
os.unlink(REMOTE_SOCKET)
|
|
1058
|
+
server = NoSignalServer(
|
|
1059
|
+
uvicorn.Config(loopback_to_api(remote), uds=REMOTE_SOCKET, **log_kwargs)
|
|
989
1060
|
)
|
|
990
|
-
|
|
991
|
-
|
|
1061
|
+
else:
|
|
1062
|
+
# Windows: checkpointer/store over TCP, no /loopback client.
|
|
1063
|
+
server = NoSignalServer(
|
|
1064
|
+
uvicorn.Config(remote, host="127.0.0.1", port=REMOTE_PORT, **log_kwargs)
|
|
1065
|
+
)
|
|
1066
|
+
try:
|
|
1067
|
+
await server.serve()
|
|
1068
|
+
finally:
|
|
1069
|
+
if REMOTE_SOCKET:
|
|
1070
|
+
with contextlib.suppress(FileNotFoundError):
|
|
1071
|
+
os.unlink(REMOTE_SOCKET)
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
LOOPBACK_PREFIX = "/loopback"
|
|
1075
|
+
"""Path on the Unix remote socket under which JS graphs reach this server's API."""
|
|
1076
|
+
|
|
1077
|
+
LOOPBACK_RUN_HEADER = b"x-langgraph-loopback-run"
|
|
1078
|
+
"""Header the JS worker sends so loopback calls run as the stream's user."""
|
|
1079
|
+
|
|
1080
|
+
_LOOPBACK_GRANTS: dict[str, Auth.types.BaseAuthContext | None] = {}
|
|
1081
|
+
"""Live JS graph streams, and the auth context each stream's loopback calls use."""
|
|
1082
|
+
|
|
1083
|
+
_MISSING = object()
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
@contextlib.contextmanager
|
|
1087
|
+
def loopback_grant() -> Iterator[str]:
|
|
1088
|
+
"""Map one JS graph stream to the run's auth context.
|
|
1089
|
+
|
|
1090
|
+
Python graphs' in-process calls keep the run's AuthContext ContextVar
|
|
1091
|
+
because they stay on the same asyncio task. A JS graph's call arrives on
|
|
1092
|
+
the Unix-socket uvicorn task, where that var is empty, so the stream
|
|
1093
|
+
carries this token and loopback_to_api restores the context.
|
|
1094
|
+
"""
|
|
1095
|
+
token = secrets.token_urlsafe(32)
|
|
1096
|
+
_LOOPBACK_GRANTS[token] = get_auth_ctx()
|
|
1097
|
+
try:
|
|
1098
|
+
yield token
|
|
1099
|
+
finally:
|
|
1100
|
+
_LOOPBACK_GRANTS.pop(token, None)
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def _loopback_api_path(path: str) -> str | None:
|
|
1104
|
+
"""Return the API path for a `/loopback/...` request, or `None` if it isn't one."""
|
|
1105
|
+
if path == LOOPBACK_PREFIX:
|
|
1106
|
+
return "/"
|
|
1107
|
+
if not path.startswith(f"{LOOPBACK_PREFIX}/"):
|
|
1108
|
+
return None
|
|
1109
|
+
return path.removeprefix(LOOPBACK_PREFIX) or "/"
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def _loopback_grant(
|
|
1113
|
+
scope: types.Scope, grants: Mapping[str, Auth.types.BaseAuthContext | None]
|
|
1114
|
+
) -> Auth.types.BaseAuthContext | None | object:
|
|
1115
|
+
"""The run's auth context, or _MISSING if this isn't a live loopback call."""
|
|
1116
|
+
token: str | None = None
|
|
1117
|
+
for name, value in scope.get("headers") or ():
|
|
1118
|
+
if name == LOOPBACK_RUN_HEADER:
|
|
1119
|
+
token = value.decode()
|
|
1120
|
+
break
|
|
1121
|
+
if token is None or token not in grants:
|
|
1122
|
+
return _MISSING
|
|
1123
|
+
return grants[token]
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
def loopback_to_api(
|
|
1127
|
+
remote: types.ASGIApp,
|
|
1128
|
+
grants: Mapping[str, Auth.types.BaseAuthContext | None] | None = None,
|
|
1129
|
+
) -> types.ASGIApp:
|
|
1130
|
+
"""Serve JS graphs' SDK calls in process, the way Python graphs' `get_client()` does.
|
|
1131
|
+
|
|
1132
|
+
A Python graph's SDK client reaches the API in memory at `root_path="/noauth"`,
|
|
1133
|
+
with its run's auth context still set. A JS graph sends the same calls over
|
|
1134
|
+
the Unix remote socket; a live grant header restores that run's user
|
|
1135
|
+
before the API handler runs. Unix only. Windows does not mount this.
|
|
1136
|
+
"""
|
|
1137
|
+
if grants is None:
|
|
1138
|
+
grants = _LOOPBACK_GRANTS
|
|
1139
|
+
|
|
1140
|
+
async def app(scope: types.Scope, receive: types.Receive, send: types.Send) -> None:
|
|
1141
|
+
api_path = (
|
|
1142
|
+
_loopback_api_path(scope.get("path", ""))
|
|
1143
|
+
if scope["type"] == "http"
|
|
1144
|
+
else None
|
|
1145
|
+
)
|
|
1146
|
+
ctx = _loopback_grant(scope, grants) if api_path is not None else _MISSING
|
|
1147
|
+
if api_path is None or ctx is _MISSING:
|
|
1148
|
+
await remote(scope, receive, send)
|
|
1149
|
+
return
|
|
1150
|
+
from langgraph_api.server import app as api # noqa: PLC0415
|
|
1151
|
+
|
|
1152
|
+
scope = {
|
|
1153
|
+
**scope,
|
|
1154
|
+
"root_path": "/noauth",
|
|
1155
|
+
"path": api_path,
|
|
1156
|
+
"raw_path": api_path.encode(),
|
|
1157
|
+
}
|
|
1158
|
+
reset = AuthContext.set(cast("Auth.types.BaseAuthContext | None", ctx))
|
|
1159
|
+
try:
|
|
1160
|
+
await api(scope, receive, send)
|
|
1161
|
+
finally:
|
|
1162
|
+
AuthContext.reset(reset)
|
|
1163
|
+
|
|
1164
|
+
return app
|
|
992
1165
|
|
|
993
1166
|
|
|
994
1167
|
class DisableHttpxLoggingContextManager(contextlib.AbstractContextManager):
|
|
@@ -1023,11 +1196,7 @@ async def wait_until_js_ready(
|
|
|
1023
1196
|
limits=httpx.Limits(max_connections=1),
|
|
1024
1197
|
transport=httpx.AsyncHTTPTransport(verify=SSL),
|
|
1025
1198
|
) as graph_client,
|
|
1026
|
-
|
|
1027
|
-
base_url=f"http://localhost:{REMOTE_PORT}",
|
|
1028
|
-
limits=httpx.Limits(max_connections=1),
|
|
1029
|
-
transport=httpx.AsyncHTTPTransport(verify=SSL),
|
|
1030
|
-
) as checkpointer_client,
|
|
1199
|
+
_checkpointer_httpx_client() as checkpointer_client,
|
|
1031
1200
|
):
|
|
1032
1201
|
attempt = 0
|
|
1033
1202
|
graph_ready = False
|
|
@@ -1088,11 +1257,7 @@ async def js_healthcheck():
|
|
|
1088
1257
|
limits=httpx.Limits(max_connections=1),
|
|
1089
1258
|
transport=httpx.AsyncHTTPTransport(verify=SSL),
|
|
1090
1259
|
) as graph_client,
|
|
1091
|
-
|
|
1092
|
-
base_url=f"http://localhost:{REMOTE_PORT}",
|
|
1093
|
-
limits=httpx.Limits(max_connections=1),
|
|
1094
|
-
transport=httpx.AsyncHTTPTransport(verify=SSL),
|
|
1095
|
-
) as checkpointer_client,
|
|
1260
|
+
_checkpointer_httpx_client() as checkpointer_client,
|
|
1096
1261
|
):
|
|
1097
1262
|
graph_passed = False
|
|
1098
1263
|
try:
|
langgraph_api/metadata.py
CHANGED
|
@@ -155,6 +155,12 @@ def _lang_usage_metadata() -> tuple[dict[str, str], dict[str, int]]:
|
|
|
155
155
|
return tags, measures
|
|
156
156
|
|
|
157
157
|
|
|
158
|
+
def _billing_channel() -> str:
|
|
159
|
+
# A self-hosted LangSmith bills API-key usage under its own license unless the
|
|
160
|
+
# server already reports it under a license key.
|
|
161
|
+
return "license_key" if LANGGRAPH_CLOUD_LICENSE_KEY else "api_key"
|
|
162
|
+
|
|
163
|
+
|
|
158
164
|
def incr_runs(*, incr: int = 1) -> None:
|
|
159
165
|
global RUN_COUNTER
|
|
160
166
|
RUN_COUNTER += incr
|
|
@@ -283,6 +289,7 @@ async def metadata_loop() -> None:
|
|
|
283
289
|
langchain_payload = {
|
|
284
290
|
**base_payload,
|
|
285
291
|
"api_key": LANGSMITH_CONTROL_PLANE_API_KEY,
|
|
292
|
+
"billing_channel": _billing_channel(),
|
|
286
293
|
}
|
|
287
294
|
submissions_attempted.append("langchain")
|
|
288
295
|
try:
|
langgraph_api/models/run.py
CHANGED
|
@@ -233,15 +233,12 @@ async def create_valid_run(
|
|
|
233
233
|
detail="Cannot specify both configurable and context. Prefer setting context alone. Context was introduced in LangGraph 0.6.0 and is the long term planned replacement for configurable.",
|
|
234
234
|
)
|
|
235
235
|
|
|
236
|
-
# ``
|
|
237
|
-
#
|
|
238
|
-
#
|
|
239
|
-
#
|
|
240
|
-
# without a transformer) or leak v2-shaped events into v1 consumers
|
|
241
|
-
# (``tools``). Reject up front so callers get a clear 422 instead of
|
|
242
|
-
# a silently empty stream.
|
|
236
|
+
# ``lifecycle`` is emitted only by the protocol v2 streaming pipeline
|
|
237
|
+
# (``stream_v2.py`` plus the registered stream transformers), so legacy
|
|
238
|
+
# routes reject it up front instead of returning a silently empty stream.
|
|
239
|
+
# ``tools`` is accepted on legacy routes and dropped in ``stream.py``.
|
|
243
240
|
if not configurable.get(EVENT_STREAMING_V2_CONFIG_KEY):
|
|
244
|
-
v2_only = {"
|
|
241
|
+
v2_only = {"lifecycle"}.intersection(stream_mode)
|
|
245
242
|
if v2_only:
|
|
246
243
|
raise HTTPException(
|
|
247
244
|
status_code=422,
|
langgraph_api/stream.py
CHANGED
|
@@ -265,6 +265,11 @@ async def astream_state(
|
|
|
265
265
|
# ``capabilities.py`` documents this behavior — keep them in sync.
|
|
266
266
|
stream_modes_set.discard("tools")
|
|
267
267
|
stream_modes_set.discard("lifecycle")
|
|
268
|
+
elif not event_streaming_v2_run:
|
|
269
|
+
# The v1 React ``useStream`` hook asks for ``tools`` whenever an app
|
|
270
|
+
# listens for tool events, but it cannot parse the events graphs emit
|
|
271
|
+
# for this mode and throws on the first one.
|
|
272
|
+
stream_modes_set.discard("tools")
|
|
268
273
|
if "debug" not in stream_modes_set:
|
|
269
274
|
stream_modes_set.add("debug")
|
|
270
275
|
if "messages-tuple" in stream_modes_set and not is_remote_pregel:
|
|
@@ -738,23 +743,42 @@ async def _consume_inline(
|
|
|
738
743
|
This preserves natural backpressure: each event must be published before the
|
|
739
744
|
next one is consumed.
|
|
740
745
|
"""
|
|
746
|
+
publishing = True
|
|
741
747
|
async with aclosing(stream):
|
|
742
748
|
try:
|
|
743
749
|
async for mode, payload in stream:
|
|
750
|
+
if not publishing:
|
|
751
|
+
continue
|
|
744
752
|
event_type = get_stream_event_type(mode)
|
|
745
753
|
payload_bytes = await run_in_executor(None, json_dumpb, payload)
|
|
746
|
-
is_resumable = resumable and
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
754
|
+
is_resumable = resumable and event_type.split("/")[0] in stream_modes
|
|
755
|
+
try:
|
|
756
|
+
with reporter.track_latency_ms(
|
|
757
|
+
LATENCY_STREAM_PUBLISH,
|
|
758
|
+
attributes={"stream_event_type": event_type},
|
|
759
|
+
):
|
|
760
|
+
await Runs.Stream.publish(
|
|
761
|
+
run_id,
|
|
762
|
+
mode,
|
|
763
|
+
payload_bytes,
|
|
764
|
+
thread_id=thread_id,
|
|
765
|
+
resumable=is_resumable,
|
|
766
|
+
)
|
|
767
|
+
except StreamPublishException as e:
|
|
768
|
+
# Stop publishing for the rest of the run: transient errors
|
|
769
|
+
# were already retried for up to a minute, and retrying every
|
|
770
|
+
# later event would stall the graph again each time. The run
|
|
771
|
+
# still finishes, and the worker records its real outcome.
|
|
772
|
+
publishing = False
|
|
773
|
+
await logger.aerror(
|
|
774
|
+
"Stream publish failed, finishing the run without streaming",
|
|
775
|
+
run_id=str(run_id),
|
|
776
|
+
thread_id=str(thread_id),
|
|
777
|
+
error_type=type(e).__name__,
|
|
778
|
+
error=str(e),
|
|
757
779
|
)
|
|
780
|
+
reporter.inc_counter(COUNTER_STREAMING_DATA_LOSS)
|
|
781
|
+
continue
|
|
758
782
|
reporter.record_histogram(
|
|
759
783
|
HISTOGRAM_STREAM_DATA_SIZE,
|
|
760
784
|
float(len(payload_bytes)),
|
|
@@ -763,26 +787,6 @@ async def _consume_inline(
|
|
|
763
787
|
except Exception as e:
|
|
764
788
|
if isinstance(e, ExceptionGroup):
|
|
765
789
|
e = e.exceptions[0]
|
|
766
|
-
if isinstance(e, StreamPublishException):
|
|
767
|
-
# We swallow this and report the run as complete, so the worker
|
|
768
|
-
# never sees it and cannot surface it. Emit it to the client here.
|
|
769
|
-
with suppress(Exception):
|
|
770
|
-
await Runs.Stream.publish(
|
|
771
|
-
run_id,
|
|
772
|
-
"error",
|
|
773
|
-
await run_in_executor(None, json_dumpb, e),
|
|
774
|
-
thread_id=thread_id,
|
|
775
|
-
resumable=resumable,
|
|
776
|
-
)
|
|
777
|
-
await logger.aerror(
|
|
778
|
-
"Suppressing stream consume exception",
|
|
779
|
-
run_id=str(run_id),
|
|
780
|
-
thread_id=str(thread_id),
|
|
781
|
-
error_type=type(e).__name__,
|
|
782
|
-
error=str(e),
|
|
783
|
-
)
|
|
784
|
-
reporter.inc_counter(COUNTER_STREAMING_DATA_LOSS)
|
|
785
|
-
return
|
|
786
790
|
raise e
|
|
787
791
|
|
|
788
792
|
|
|
@@ -858,7 +862,7 @@ async def _consume_async_queue(
|
|
|
858
862
|
async for mode, payload in stream:
|
|
859
863
|
event_type = get_stream_event_type(mode)
|
|
860
864
|
payload_bytes = await run_in_executor(None, json_dumpb, payload)
|
|
861
|
-
is_resumable = resumable and
|
|
865
|
+
is_resumable = resumable and event_type.split("/")[0] in stream_modes
|
|
862
866
|
await _enqueue((mode, payload_bytes, event_type, is_resumable))
|
|
863
867
|
|
|
864
868
|
except Exception as e:
|
langgraph_api/worker.py
CHANGED
|
@@ -296,6 +296,15 @@ async def worker(
|
|
|
296
296
|
"Background run timed out. To increase the timeout, set the BG_JOB_TIMEOUT_SECS environment variable (integer, defaults to 86400).",
|
|
297
297
|
**log_info,
|
|
298
298
|
)
|
|
299
|
+
# Without it, every stream shows a timed-out run as a success.
|
|
300
|
+
with suppress(Exception):
|
|
301
|
+
await Runs.Stream.publish(
|
|
302
|
+
run_id,
|
|
303
|
+
"error",
|
|
304
|
+
json_dumpb(exception),
|
|
305
|
+
thread_id=run["thread_id"],
|
|
306
|
+
resumable=resumable,
|
|
307
|
+
)
|
|
299
308
|
if not temporary:
|
|
300
309
|
await Threads.set_joint_status(
|
|
301
310
|
conn,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langgraph-api
|
|
3
|
-
Version: 0.18.0.
|
|
3
|
+
Version: 0.18.0.dev12
|
|
4
4
|
Author-email: Will Fu-Hinthorn <will@langchain.dev>, Josh Rogers <josh@langchain.dev>, Parker Rule <parker@langchain.dev>
|
|
5
5
|
License: Elastic-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -24,7 +24,7 @@ Requires-Dist: langsmith[otel]>=0.6.3
|
|
|
24
24
|
Requires-Dist: mcp<3,>=2.3.0
|
|
25
25
|
Requires-Dist: opentelemetry-api>=0.0.1
|
|
26
26
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=0.0.1
|
|
27
|
-
Requires-Dist: opentelemetry-exporter-prometheus<0.
|
|
27
|
+
Requires-Dist: opentelemetry-exporter-prometheus<0.67,>=0.63b1
|
|
28
28
|
Requires-Dist: opentelemetry-sdk>=0.0.1
|
|
29
29
|
Requires-Dist: orjson>=3.9.7
|
|
30
30
|
Requires-Dist: packaging>=23.2
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
langgraph_api/__init__.py,sha256
|
|
1
|
+
langgraph_api/__init__.py,sha256=4-U3UVsECE8ZPJ6VZOCr58LOX8rIvYtmAXI_APYqlGo,29
|
|
2
2
|
langgraph_api/_factory_utils.py,sha256=_FqW0olMu2PGLsnRq0ylt3a4jDCe12YIFHExMXI6Nyo,5976
|
|
3
3
|
langgraph_api/asgi_transport.py,sha256=XApY3lIWBZTMbbsl8dDJzl0cLGirmAGE0SifqZUnXvs,11896
|
|
4
4
|
langgraph_api/asyncio.py,sha256=-p-Vx89TnErypdPcWb-2EdbsSvhdIdHfriCFG7UP7ls,12444
|
|
@@ -10,12 +10,12 @@ langgraph_api/cron_scheduler.py,sha256=H1jPymC68A1MYyjfdApX4-hELiGBlJpGgSOsN8qhc
|
|
|
10
10
|
langgraph_api/drain.py,sha256=MLUm3fDq3R8yxrZ-1W3TeCfhNzgNSzUA7TvB-D6K2_s,6108
|
|
11
11
|
langgraph_api/errors.py,sha256=x6yhf0JrgSNweFRJw7l3ZK8De4T76d2v6MRvoDU_C-U,3312
|
|
12
12
|
langgraph_api/feature_flags.py,sha256=NB1X5jO19c40hI3WDxGk9qwD6EZUROO0ooFSd1QTniM,3985
|
|
13
|
-
langgraph_api/graph.py,sha256=
|
|
13
|
+
langgraph_api/graph.py,sha256=aWj1o4bYU2prIWpAVAWOH7CHbjvZKYfWV2CtF-hzPko,40697
|
|
14
14
|
langgraph_api/http.py,sha256=7hPxKbj-xoAKcm7iucBpT5nM_hXOgGVCPbBsCD693Cw,6977
|
|
15
15
|
langgraph_api/http_metrics.py,sha256=etxbZNmYxdb58DVLNkHP7S-N6njXPTiQh2OWKMaIZi8,5336
|
|
16
16
|
langgraph_api/http_metrics_utils.py,sha256=sjxF7SYGTzY0Wz_G0dzatsYNnWr31S6ujej4JmBG2yo,866
|
|
17
17
|
langgraph_api/logging.py,sha256=PeApafcPxox03rcpjEU5rlSzDkxJ3gff52XC34uZjl0,6972
|
|
18
|
-
langgraph_api/metadata.py,sha256=
|
|
18
|
+
langgraph_api/metadata.py,sha256=j4FJvZRJP5tPi6uBcmLPaI4LrrhIZ7Df1ccoTYY_no4,11976
|
|
19
19
|
langgraph_api/metrics_collector.py,sha256=ALUyirKew3uqtXVrCwpBc7wCM_viNhT13wZVMUVyva0,8495
|
|
20
20
|
langgraph_api/metrics_otlp.py,sha256=JucxN4i4qn79jP4td0A_T6ycFpLP1G7GThvQTczfpAA,30850
|
|
21
21
|
langgraph_api/operator_routes.py,sha256=s4HfkNUdMvgyYPb8L8yM3Z8UJQbGkJrVFMiH5HJp_S4,8389
|
|
@@ -32,13 +32,13 @@ langgraph_api/server.py,sha256=_DG7dCL2wtlaXdppTce2e0k8PunCcSRj79ncQfqmK_Q,10305
|
|
|
32
32
|
langgraph_api/sse.py,sha256=cChZ7raQUHp8p5BreE_5wMBR8lFO0n7746sV8_HQOrc,4822
|
|
33
33
|
langgraph_api/state.py,sha256=Py8HG8k7u0FRAj4Ok89_uR9GerkyeFm90gSMTbWWQX0,8356
|
|
34
34
|
langgraph_api/store.py,sha256=OXcdQSVCDKOzfS8S2aJHXbfuI5iYRoQBiW9WYKnCyx8,7122
|
|
35
|
-
langgraph_api/stream.py,sha256=
|
|
35
|
+
langgraph_api/stream.py,sha256=XnXqtf628r5EUltaV9JtKlka_40YO38kkYUNUTz4hcQ,36891
|
|
36
36
|
langgraph_api/stream_v2.py,sha256=AieeCAXQsthy9n6crCibMB7unbN1W_o1xkpJ2v11RSY,10822
|
|
37
37
|
langgraph_api/traceblock.py,sha256=GhgAtLhAQ8Z8LHtRQq3par-rfPpRBQp2Q7WBACJq1NI,677
|
|
38
38
|
langgraph_api/tracing_session.py,sha256=gmfv-BW1XRar6suVur1P4rTVKWkjElDEb2hhkBduWOs,728
|
|
39
39
|
langgraph_api/validation.py,sha256=_fmqjbETfsrc4KKJLtfhVjELD-1WzEVrK6G1pfDrmtk,14930
|
|
40
40
|
langgraph_api/webhook.py,sha256=qXEtkE6orek2POeOQmPRsEarJgXIYp-LBrZB-OwITxc,9572
|
|
41
|
-
langgraph_api/worker.py,sha256=
|
|
41
|
+
langgraph_api/worker.py,sha256=GjHJ1ig--xdCBg065W1JAfXSzgt3Ec-0iX7lbU31CIM,22457
|
|
42
42
|
langgraph_api/worker_runtime.py,sha256=zzVW7A4vHyJE6DRxdjRZIa2U334Eh5oO157SyWmQ3G8,11861
|
|
43
43
|
langgraph_api/_checkpointer/__init__.py,sha256=ofJTJLGy7Hsuzhj-2dpfDvrDloM0BzlhTzvZOdR9K8U,2223
|
|
44
44
|
langgraph_api/_checkpointer/_adapter.py,sha256=idGYL-FwyVtUXcmhvfYboARfQAkoj0aFD40L5h9N7F8,22289
|
|
@@ -53,7 +53,7 @@ langgraph_api/api/openapi.py,sha256=hS4yhfJfWAYilm0Pv8iC08bK7CMDG4PIVc-77y9Y7Ts,
|
|
|
53
53
|
langgraph_api/api/profile.py,sha256=CA1ZkHALOuP8orYTICnEhcG_JnnA2wnyjbWyeb117jA,3455
|
|
54
54
|
langgraph_api/api/runs.py,sha256=0ogQnEQyKeVTuEUY6UD4KMShUA6Qq7_Zyp0Z70zGSzs,32577
|
|
55
55
|
langgraph_api/api/store.py,sha256=5d1GOxsVDK-8PPngYG2rjYF_B2qNevDvPos-nB8YPCI,7710
|
|
56
|
-
langgraph_api/api/threads.py,sha256=
|
|
56
|
+
langgraph_api/api/threads.py,sha256=JT7TpAs7zJBokxzGl_DmUNEnfAVfTRdJRiLwuP4D1EM,22916
|
|
57
57
|
langgraph_api/api/ui.py,sha256=A1cRxaMK_4pb192mrZ_LMxLSFb8NriYwwvjeDdNDD7k,2803
|
|
58
58
|
langgraph_api/api/a2ui_schemas/README.md,sha256=fZCc9rSK-qeUHsbJ17EYxMZjS3yUrYQdoQLX7ltgP8M,298
|
|
59
59
|
langgraph_api/api/a2ui_schemas/client_capabilities.json,sha256=3YAHlRfeOn9dUH6uAek1loCL4caeSUuQzhrtWKWW9nU,1906
|
|
@@ -100,15 +100,15 @@ langgraph_api/event_streaming/types.py,sha256=RyZqfqgH-jmmmmAFQj5f6nH9M1rGK93zVG
|
|
|
100
100
|
langgraph_api/executor/__init__.py,sha256=mZBT8HSqe9pMlBpE7sw6DMI4ULB4xwNPWGkN9zp1B7k,302
|
|
101
101
|
langgraph_api/executor/__main__.py,sha256=fKZs36-aLdXMZrRrXOneBJNV88maiNwuToRCIoyHzmQ,12091
|
|
102
102
|
langgraph_api/grpc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
103
|
-
langgraph_api/grpc/client.py,sha256=
|
|
103
|
+
langgraph_api/grpc/client.py,sha256=L-FM_NrfO-pJHIk71g-rbIcR10tzCzJPv9VOszZoIkc,16100
|
|
104
104
|
langgraph_api/grpc/server.py,sha256=Fknr6BJbdSK38wF7B1vPDplK25KB_UfMB9Fp1er6DPU,8366
|
|
105
105
|
langgraph_api/grpc/generated/core_api_pb2.pyi,sha256=0PEzVh8s6rqHq52ekhqPaMI_TOsP4_xuUUv9_XS7r1w,54758
|
|
106
106
|
langgraph_api/grpc/ops/__init__.py,sha256=KQL4w0xjvkCVz4j770QQVuP7MQh6qPaTZnxlO3KmoiA,18754
|
|
107
107
|
langgraph_api/grpc/ops/assistants.py,sha256=hLDZrAahpGSjzWNzr8nQidTW_4MoCR6sZy5VWB7oRxo,14080
|
|
108
108
|
langgraph_api/grpc/ops/cache.py,sha256=MoPAmSuOwbt2Cud1YZJm_YJeaDHPFy1IiV4m0PDImgg,1399
|
|
109
109
|
langgraph_api/grpc/ops/crons.py,sha256=aDlvNVYIfSP8gtr5NptLiAj-bfMtJOuMWYIA2NifWm0,19713
|
|
110
|
-
langgraph_api/grpc/ops/runs.py,sha256=
|
|
111
|
-
langgraph_api/grpc/ops/threads.py,sha256=
|
|
110
|
+
langgraph_api/grpc/ops/runs.py,sha256=LM4tbxTUZM_HvTuBQ7AkhWa95Zza4jWw2ULcnNgeUoY,44747
|
|
111
|
+
langgraph_api/grpc/ops/threads.py,sha256=4XPd4jzfKp77KY5BY6W0nbgXj2cK7wKY7uhON12zMUg,52153
|
|
112
112
|
langgraph_api/grpc/servicers/__init__.py,sha256=l8yqTA_gMbIj0xHw5-RZYo0JbjL-EpFD8RxPMgW_-bA,422
|
|
113
113
|
langgraph_api/grpc/servicers/checkpointer.py,sha256=kn9J3HQEmqggtOGiDxpEAeBVkVi4qDCJikj2bXo5k4g,10543
|
|
114
114
|
langgraph_api/grpc/servicers/encryption.py,sha256=ddAjRZ9btWBf4MPa7yd793japdCEi-DfEPi1Ynt8gzk,10563
|
|
@@ -119,11 +119,11 @@ langgraph_api/js/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
|
|
|
119
119
|
langgraph_api/js/base.py,sha256=CJihwc51MwOVkis80f8zudRa1fQz_5jrom4rY8trww8,1133
|
|
120
120
|
langgraph_api/js/build.mts,sha256=--Kfm4NKDhMljmv8fVvRR0VeUsBKiWch6qXpBoKmrvU,3445
|
|
121
121
|
langgraph_api/js/client.http.mts,sha256=ARlTJURLJamgvyaPlBCYEAHFnXQnPpzzUXWKwXiebnw,6011
|
|
122
|
-
langgraph_api/js/client.mts,sha256=
|
|
122
|
+
langgraph_api/js/client.mts,sha256=oM3ctxVJ8YoxEDmjWAlZqVDv28k9TVUSD0dezsA58y0,41978
|
|
123
123
|
langgraph_api/js/errors.py,sha256=Cm1TKWlUCwZReDC5AQ6SgNIVGD27Qov2xcgHyf8-GXo,361
|
|
124
|
-
langgraph_api/js/global.d.ts,sha256=
|
|
124
|
+
langgraph_api/js/global.d.ts,sha256=LHCvS5NFvr0KKJUFzqIGL-CdBC4CgFIqif1im1r9xAg,237
|
|
125
125
|
langgraph_api/js/package.json,sha256=e2FzqGICzYOtz3CsncKR8o4trBP6GMCZUU8RdbWMvSI,1372
|
|
126
|
-
langgraph_api/js/remote.py,sha256=
|
|
126
|
+
langgraph_api/js/remote.py,sha256=WR2hss2HL2j3FMWqfUTqleK_LXGSCNets1LH6ub8xMQ,59394
|
|
127
127
|
langgraph_api/js/schema.py,sha256=M4fLtr50O1jck8H1hm_0W4cZOGYGdkrB7riLyCes4oY,438
|
|
128
128
|
langgraph_api/js/sse.py,sha256=IfcEpknyZeqqjYTOWRdijc6o2EBfatbWCZarGQQ0SQI,4088
|
|
129
129
|
langgraph_api/js/traceblock.mts,sha256=QtGSN5VpzmGqDfbArrGXkMiONY94pMQ5CgzetT_bKYg,761
|
|
@@ -149,7 +149,7 @@ langgraph_api/middleware/http_logger.py,sha256=79iMlqKdSP5sS6G-iuEAHl2h0Z2ygYDxs
|
|
|
149
149
|
langgraph_api/middleware/private_network.py,sha256=eQEzWI8epBNUCiNsMu9O27ofHBQ45M0p2OZy5YdUYos,2097
|
|
150
150
|
langgraph_api/middleware/request_id.py,sha256=-p230Q5jDJAJLmSZRqQvB4dFFkJS9B4Vwg6pUgQtI24,1259
|
|
151
151
|
langgraph_api/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
152
|
-
langgraph_api/models/run.py,sha256=
|
|
152
|
+
langgraph_api/models/run.py,sha256=XHlx9m3Odr61CGuHjNdAqJEU_B325rxRn-98AeAgV4M,15625
|
|
153
153
|
langgraph_api/timing/__init__.py,sha256=UDMuNBmc5NqX0m5T-9PUFioCa1lNvd4Wv4juPIBRb-w,877
|
|
154
154
|
langgraph_api/timing/profiler.py,sha256=_n0qJYJMjCGo95RDuH5z8FIzePGNSNbnm3cvGmcgn6A,6186
|
|
155
155
|
langgraph_api/timing/startup.py,sha256=1Jp4-tquBhiE5glWQhXGX5XS2_006myJXHHZMOi15ng,6161
|
|
@@ -182,11 +182,11 @@ langgraph_runtime/store.py,sha256=7mowndlsIroGHv3NpTSOZDJR0lCuaYMBoTnTrewjslw,11
|
|
|
182
182
|
LICENSE,sha256=SCVQGLQfwOllsRFa9-Z3m8IYu4pnR9Vh2oANUCJiKqI,3860
|
|
183
183
|
hatch_build.py,sha256=Nn9N-EFfVqmH5q3iCUrhqX6zc7qT1jLI3gWSBN2xgYQ,1738
|
|
184
184
|
logging.json,sha256=sBy8HDDPuucpLbLUD6e8t5fsiQjJoklZsbVHi2qQ7aQ,367
|
|
185
|
-
openapi.json,sha256
|
|
185
|
+
openapi.json,sha256=AX8OyPP9M3JCJ4J09DU0KdmPxTTlHl12YLm8e-u7Fg4,229573
|
|
186
186
|
langgraph_grpc_common/__init__.py,sha256=sO7jFHekQxt6zZPhTPZAWLYLB_S8w7-VuJ7NyWFlFNo,97
|
|
187
187
|
langgraph_grpc_common/checkpointer.py,sha256=FwY4a4zIdRFy_Fw2jd_dxIhBpWBIMi5kJqye2VkzzUg,16505
|
|
188
188
|
langgraph_grpc_common/serde.py,sha256=8q290nZ6TdxbEhCTzwEs44XGi2b-3pfrUeMYY5WGlTY,6179
|
|
189
|
-
langgraph_grpc_common/store.py,sha256=
|
|
189
|
+
langgraph_grpc_common/store.py,sha256=yyckUNGX9-2ZoU1XiuhPHpK600MNPUWV-NYoK1BUzHA,5063
|
|
190
190
|
langgraph_grpc_common/conversion/__init__.py,sha256=pLsOrRtmF9YkgmCPhcfym6lYOB7PCJTRItyo1YK_AT8,208
|
|
191
191
|
langgraph_grpc_common/conversion/_compat.py,sha256=MIEo69I7tfaPskunPGwBCKheksLwxpw-z-4O3hvO9og,4010
|
|
192
192
|
langgraph_grpc_common/conversion/checkpoint.py,sha256=aBiQ_73PA4HUFFzgnJ9FKhWuqUhPHjyN9aPKrqd7LNs,13760
|
|
@@ -260,8 +260,8 @@ langgraph_grpc_common/proto/store_pb2.py,sha256=VWJ7wtzL9pVmZZKbxMzOzCoiFn58bB2g
|
|
|
260
260
|
langgraph_grpc_common/proto/store_pb2.pyi,sha256=atXejijuYEb9tE0stGQdZcfA9ucMFj-ytTMwvkxwviM,29322
|
|
261
261
|
langgraph_grpc_common/proto/store_pb2_grpc.py,sha256=Kyt8eV2NXkjh3be_BYytGdD19kNf74961cruonMvCrw,3300
|
|
262
262
|
langgraph_grpc_common/proto/store_pb2_grpc.pyi,sha256=oS1h2cDoq2OjyM8xazcmc8LwRDC5oCAD0zto2QUmPQw,2027
|
|
263
|
-
langgraph_api-0.18.0.
|
|
264
|
-
langgraph_api-0.18.0.
|
|
265
|
-
langgraph_api-0.18.0.
|
|
266
|
-
langgraph_api-0.18.0.
|
|
267
|
-
langgraph_api-0.18.0.
|
|
263
|
+
langgraph_api-0.18.0.dev12.dist-info/METADATA,sha256=R4amunHvob9GwNDTv0iaNt8y_qlFejxLKP_JLQDCL0s,5940
|
|
264
|
+
langgraph_api-0.18.0.dev12.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
265
|
+
langgraph_api-0.18.0.dev12.dist-info/entry_points.txt,sha256=hGedv8n7cgi41PypMfinwS_HfCwA7xJIfS0jAp8htV8,78
|
|
266
|
+
langgraph_api-0.18.0.dev12.dist-info/licenses/LICENSE,sha256=SCVQGLQfwOllsRFa9-Z3m8IYu4pnR9Vh2oANUCJiKqI,3860
|
|
267
|
+
langgraph_api-0.18.0.dev12.dist-info/RECORD,,
|
langgraph_grpc_common/store.py
CHANGED
|
@@ -62,8 +62,15 @@ class GrpcStore(AsyncBatchedBaseStore):
|
|
|
62
62
|
if self._stub is None:
|
|
63
63
|
with self._channel_lock:
|
|
64
64
|
if self._stub is None:
|
|
65
|
-
# Internal loopback to core-server
|
|
66
|
-
|
|
65
|
+
# Internal loopback to core-server, reconnecting as fast as
|
|
66
|
+
# langgraph_api.grpc.client does (which explains the values).
|
|
67
|
+
self._channel = grpc.aio.insecure_channel(
|
|
68
|
+
self._address,
|
|
69
|
+
options=[
|
|
70
|
+
("grpc.initial_reconnect_backoff_ms", 100),
|
|
71
|
+
("grpc.max_reconnect_backoff_ms", 1000),
|
|
72
|
+
],
|
|
73
|
+
)
|
|
67
74
|
self._stub = StoreStub(self._channel)
|
|
68
75
|
return self._stub
|
|
69
76
|
|
openapi.json
CHANGED
|
@@ -1666,6 +1666,28 @@
|
|
|
1666
1666
|
},
|
|
1667
1667
|
"name": "stream_modes",
|
|
1668
1668
|
"in": "query"
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
"required": false,
|
|
1672
|
+
"schema": {
|
|
1673
|
+
"anyOf": [
|
|
1674
|
+
{
|
|
1675
|
+
"type": "string",
|
|
1676
|
+
"enum": ["lifecycle", "run_modes", "state_update"]
|
|
1677
|
+
},
|
|
1678
|
+
{
|
|
1679
|
+
"type": "array",
|
|
1680
|
+
"items": {
|
|
1681
|
+
"type": "string",
|
|
1682
|
+
"enum": ["lifecycle", "run_modes", "state_update"]
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
],
|
|
1686
|
+
"title": "Stream Mode",
|
|
1687
|
+
"description": "Alias of `stream_modes`, the name the SDKs send. Ignored when `stream_modes` is set."
|
|
1688
|
+
},
|
|
1689
|
+
"name": "stream_mode",
|
|
1690
|
+
"in": "query"
|
|
1669
1691
|
}
|
|
1670
1692
|
],
|
|
1671
1693
|
"responses": {
|
|
@@ -5308,7 +5330,8 @@
|
|
|
5308
5330
|
"updates",
|
|
5309
5331
|
"events",
|
|
5310
5332
|
"debug",
|
|
5311
|
-
"custom"
|
|
5333
|
+
"custom",
|
|
5334
|
+
"tools"
|
|
5312
5335
|
]
|
|
5313
5336
|
},
|
|
5314
5337
|
"type": "array"
|
|
@@ -5324,7 +5347,8 @@
|
|
|
5324
5347
|
"updates",
|
|
5325
5348
|
"events",
|
|
5326
5349
|
"debug",
|
|
5327
|
-
"custom"
|
|
5350
|
+
"custom",
|
|
5351
|
+
"tools"
|
|
5328
5352
|
]
|
|
5329
5353
|
}
|
|
5330
5354
|
],
|
|
@@ -5506,7 +5530,8 @@
|
|
|
5506
5530
|
"updates",
|
|
5507
5531
|
"events",
|
|
5508
5532
|
"debug",
|
|
5509
|
-
"custom"
|
|
5533
|
+
"custom",
|
|
5534
|
+
"tools"
|
|
5510
5535
|
]
|
|
5511
5536
|
},
|
|
5512
5537
|
"type": "array"
|
|
@@ -5522,7 +5547,8 @@
|
|
|
5522
5547
|
"updates",
|
|
5523
5548
|
"events",
|
|
5524
5549
|
"debug",
|
|
5525
|
-
"custom"
|
|
5550
|
+
"custom",
|
|
5551
|
+
"tools"
|
|
5526
5552
|
]
|
|
5527
5553
|
}
|
|
5528
5554
|
],
|
|
File without changes
|
{langgraph_api-0.18.0.dev10.dist-info → langgraph_api-0.18.0.dev12.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{langgraph_api-0.18.0.dev10.dist-info → langgraph_api-0.18.0.dev12.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|