langgraph-api 0.4.18__tar.gz → 0.4.20__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (121) hide show
  1. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/PKG-INFO +1 -1
  2. langgraph_api-0.4.20/langgraph_api/__init__.py +1 -0
  3. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/api/runs.py +163 -117
  4. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/serde.py +4 -0
  5. langgraph_api-0.4.18/langgraph_api/__init__.py +0 -1
  6. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/.gitignore +0 -0
  7. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/LICENSE +0 -0
  8. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/Makefile +0 -0
  9. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/README.md +0 -0
  10. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/benchmark/.gitignore +0 -0
  11. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/benchmark/Makefile +0 -0
  12. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/benchmark/README.md +0 -0
  13. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/benchmark/burst.js +0 -0
  14. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/benchmark/clean.js +0 -0
  15. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/benchmark/graphs.js +0 -0
  16. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/benchmark/package.json +0 -0
  17. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/benchmark/ramp.js +0 -0
  18. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/benchmark/update-revision.js +0 -0
  19. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/benchmark/weather.js +0 -0
  20. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/constraints.txt +0 -0
  21. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/forbidden.txt +0 -0
  22. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/healthcheck.py +0 -0
  23. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/api/__init__.py +0 -0
  24. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/api/a2a.py +0 -0
  25. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/api/assistants.py +0 -0
  26. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/api/mcp.py +0 -0
  27. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/api/meta.py +0 -0
  28. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/api/openapi.py +0 -0
  29. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/api/store.py +0 -0
  30. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/api/threads.py +0 -0
  31. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/api/ui.py +0 -0
  32. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/asgi_transport.py +0 -0
  33. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/asyncio.py +0 -0
  34. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/auth/__init__.py +0 -0
  35. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/auth/custom.py +0 -0
  36. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/auth/langsmith/__init__.py +0 -0
  37. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/auth/langsmith/backend.py +0 -0
  38. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/auth/langsmith/client.py +0 -0
  39. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/auth/middleware.py +0 -0
  40. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/auth/noop.py +0 -0
  41. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/auth/studio_user.py +0 -0
  42. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/cli.py +0 -0
  43. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/command.py +0 -0
  44. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/config.py +0 -0
  45. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/cron_scheduler.py +0 -0
  46. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/errors.py +0 -0
  47. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/executor_entrypoint.py +0 -0
  48. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/feature_flags.py +0 -0
  49. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/graph.py +0 -0
  50. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/http.py +0 -0
  51. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/http_metrics.py +0 -0
  52. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/.gitignore +0 -0
  53. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/.prettierrc +0 -0
  54. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/__init__.py +0 -0
  55. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/base.py +0 -0
  56. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/build.mts +0 -0
  57. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/client.http.mts +0 -0
  58. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/client.mts +0 -0
  59. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/errors.py +0 -0
  60. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/global.d.ts +0 -0
  61. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/package.json +0 -0
  62. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/remote.py +0 -0
  63. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/schema.py +0 -0
  64. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/src/graph.mts +0 -0
  65. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/src/load.hooks.mjs +0 -0
  66. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/src/preload.mjs +0 -0
  67. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/src/utils/files.mts +0 -0
  68. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/src/utils/importMap.mts +0 -0
  69. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
  70. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/src/utils/serde.mts +0 -0
  71. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/sse.py +0 -0
  72. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/traceblock.mts +0 -0
  73. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/tsconfig.json +0 -0
  74. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/ui.py +0 -0
  75. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/js/yarn.lock +0 -0
  76. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/logging.py +0 -0
  77. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/metadata.py +0 -0
  78. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/middleware/__init__.py +0 -0
  79. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/middleware/http_logger.py +0 -0
  80. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/middleware/private_network.py +0 -0
  81. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/middleware/request_id.py +0 -0
  82. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/models/__init__.py +0 -0
  83. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/models/run.py +0 -0
  84. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/patch.py +0 -0
  85. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/queue_entrypoint.py +0 -0
  86. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/route.py +0 -0
  87. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/schema.py +0 -0
  88. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/server.py +0 -0
  89. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/sse.py +0 -0
  90. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/state.py +0 -0
  91. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/store.py +0 -0
  92. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/stream.py +0 -0
  93. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/thread_ttl.py +0 -0
  94. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/traceblock.py +0 -0
  95. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/tunneling/cloudflare.py +0 -0
  96. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/utils/__init__.py +0 -0
  97. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/utils/cache.py +0 -0
  98. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/utils/config.py +0 -0
  99. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/utils/future.py +0 -0
  100. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/utils/headers.py +0 -0
  101. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/utils/retriable_client.py +0 -0
  102. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/utils/uuids.py +0 -0
  103. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/validation.py +0 -0
  104. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/webhook.py +0 -0
  105. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_api/worker.py +0 -0
  106. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_license/__init__.py +0 -0
  107. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_license/validation.py +0 -0
  108. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_runtime/__init__.py +0 -0
  109. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_runtime/checkpoint.py +0 -0
  110. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_runtime/database.py +0 -0
  111. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_runtime/lifespan.py +0 -0
  112. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_runtime/metrics.py +0 -0
  113. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_runtime/ops.py +0 -0
  114. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_runtime/queue.py +0 -0
  115. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_runtime/retry.py +0 -0
  116. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/langgraph_runtime/store.py +0 -0
  117. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/logging.json +0 -0
  118. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/openapi.json +0 -0
  119. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/pyproject.toml +0 -0
  120. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/scripts/create_license.py +0 -0
  121. {langgraph_api-0.4.18 → langgraph_api-0.4.20}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-api
3
- Version: 0.4.18
3
+ Version: 0.4.20
4
4
  Author-email: Nuno Campos <nuno@langchain.dev>, Will Fu-Hinthorn <will@langchain.dev>
5
5
  License: Elastic-2.0
6
6
  License-File: LICENSE
@@ -0,0 +1 @@
1
+ __version__ = "0.4.20"
@@ -104,7 +104,9 @@ async def stream_run(
104
104
  payload = await request.json(RunCreateStateful)
105
105
  on_disconnect = payload.get("on_disconnect", "continue")
106
106
  run_id = uuid7()
107
- async with await Runs.Stream.subscribe(run_id, thread_id) as sub:
107
+
108
+ sub = await Runs.Stream.subscribe(run_id, thread_id)
109
+ try:
108
110
  async with connect() as conn:
109
111
  run = await create_valid_run(
110
112
  conn,
@@ -114,20 +116,32 @@ async def stream_run(
114
116
  run_id=run_id,
115
117
  request_start_time=request.scope.get("request_start_time_ms"),
116
118
  )
117
-
118
- return EventSourceResponse(
119
- Runs.Stream.join(
119
+ except Exception:
120
+ # Clean up the pubsub on errors
121
+ await sub.__aexit__(None, None, None)
122
+ raise
123
+
124
+ async def body():
125
+ try:
126
+ async for event, message, stream_id in Runs.Stream.join(
120
127
  run["run_id"],
121
128
  thread_id=thread_id,
122
129
  cancel_on_disconnect=on_disconnect == "cancel",
123
130
  stream_channel=sub,
124
131
  last_event_id=None,
125
- ),
126
- headers={
127
- "Location": f"/threads/{thread_id}/runs/{run['run_id']}/stream",
128
- "Content-Location": f"/threads/{thread_id}/runs/{run['run_id']}",
129
- },
130
- )
132
+ ):
133
+ yield event, message, stream_id
134
+ finally:
135
+ # Make sure to always clean up the pubsub
136
+ await sub.__aexit__(None, None, None)
137
+
138
+ return EventSourceResponse(
139
+ body(),
140
+ headers={
141
+ "Location": f"/threads/{thread_id}/runs/{run['run_id']}/stream",
142
+ "Content-Location": f"/threads/{thread_id}/runs/{run['run_id']}",
143
+ },
144
+ )
131
145
 
132
146
 
133
147
  async def stream_run_stateless(
@@ -139,7 +153,9 @@ async def stream_run_stateless(
139
153
  on_disconnect = payload.get("on_disconnect", "continue")
140
154
  run_id = uuid7()
141
155
  thread_id = uuid4()
142
- async with await Runs.Stream.subscribe(run_id, thread_id) as sub:
156
+
157
+ sub = await Runs.Stream.subscribe(run_id, thread_id)
158
+ try:
143
159
  async with connect() as conn:
144
160
  run = await create_valid_run(
145
161
  conn,
@@ -150,21 +166,33 @@ async def stream_run_stateless(
150
166
  request_start_time=request.scope.get("request_start_time_ms"),
151
167
  temporary=True,
152
168
  )
153
-
154
- return EventSourceResponse(
155
- Runs.Stream.join(
169
+ except Exception:
170
+ # Clean up the pubsub on errors
171
+ await sub.__aexit__(None, None, None)
172
+ raise
173
+
174
+ async def body():
175
+ try:
176
+ async for event, message, stream_id in Runs.Stream.join(
156
177
  run["run_id"],
157
178
  thread_id=run["thread_id"],
158
179
  ignore_404=True,
159
180
  cancel_on_disconnect=on_disconnect == "cancel",
160
181
  stream_channel=sub,
161
182
  last_event_id=None,
162
- ),
163
- headers={
164
- "Location": f"/runs/{run['run_id']}/stream",
165
- "Content-Location": f"/runs/{run['run_id']}",
166
- },
167
- )
183
+ ):
184
+ yield event, message, stream_id
185
+ finally:
186
+ # Make sure to always clean up the pubsub
187
+ await sub.__aexit__(None, None, None)
188
+
189
+ return EventSourceResponse(
190
+ body(),
191
+ headers={
192
+ "Location": f"/runs/{run['run_id']}/stream",
193
+ "Content-Location": f"/runs/{run['run_id']}",
194
+ },
195
+ )
168
196
 
169
197
 
170
198
  @retry_db
@@ -174,7 +202,8 @@ async def wait_run(request: ApiRequest):
174
202
  payload = await request.json(RunCreateStateful)
175
203
  on_disconnect = payload.get("on_disconnect", "continue")
176
204
  run_id = uuid7()
177
- async with await Runs.Stream.subscribe(run_id, thread_id) as sub:
205
+ sub = await Runs.Stream.subscribe(run_id, thread_id)
206
+ try:
178
207
  async with connect() as conn:
179
208
  run = await create_valid_run(
180
209
  conn,
@@ -184,43 +213,44 @@ async def wait_run(request: ApiRequest):
184
213
  run_id=run_id,
185
214
  request_start_time=request.scope.get("request_start_time_ms"),
186
215
  )
187
-
188
- last_chunk = ValueEvent()
189
-
190
- async def consume():
191
- vchunk: bytes | None = None
192
- async for mode, chunk, _ in Runs.Stream.join(
193
- run["run_id"],
194
- thread_id=run["thread_id"],
195
- stream_channel=sub,
196
- cancel_on_disconnect=on_disconnect == "cancel",
197
- ):
198
- if (
199
- mode == b"values"
200
- or mode == b"updates"
201
- and b"__interrupt__" in chunk
202
- ):
203
- vchunk = chunk
204
- elif mode == b"error":
205
- vchunk = orjson.dumps({"__error__": orjson.Fragment(chunk)})
206
- if vchunk is not None:
207
- last_chunk.set(vchunk)
208
- else:
209
- async with connect() as conn:
210
- thread_iter = await Threads.get(conn, thread_id)
211
- try:
212
- thread = await anext(thread_iter)
213
- last_chunk.set(thread["values"])
214
- except StopAsyncIteration:
215
- await logger.awarning(
216
- f"No checkpoint found for thread {thread_id}",
217
- thread_id=thread_id,
218
- )
219
- last_chunk.set(b"{}")
220
-
221
- # keep the connection open by sending whitespace every 5 seconds
222
- # leading whitespace will be ignored by json parsers
223
- async def body() -> AsyncIterator[bytes]:
216
+ except Exception:
217
+ # Clean up the pubsub on errors
218
+ await sub.__aexit__(None, None, None)
219
+ raise
220
+
221
+ last_chunk = ValueEvent()
222
+
223
+ async def consume():
224
+ vchunk: bytes | None = None
225
+ async for mode, chunk, _ in Runs.Stream.join(
226
+ run["run_id"],
227
+ thread_id=run["thread_id"],
228
+ stream_channel=sub,
229
+ cancel_on_disconnect=on_disconnect == "cancel",
230
+ ):
231
+ if mode == b"values" or mode == b"updates" and b"__interrupt__" in chunk:
232
+ vchunk = chunk
233
+ elif mode == b"error":
234
+ vchunk = orjson.dumps({"__error__": orjson.Fragment(chunk)})
235
+ if vchunk is not None:
236
+ last_chunk.set(vchunk)
237
+ else:
238
+ async with connect() as conn:
239
+ thread_iter = await Threads.get(conn, thread_id)
240
+ try:
241
+ thread = await anext(thread_iter)
242
+ last_chunk.set(thread["values"])
243
+ except StopAsyncIteration:
244
+ await logger.awarning(
245
+ f"No checkpoint found for thread {thread_id}",
246
+ thread_id=thread_id,
247
+ )
248
+ last_chunk.set(b"{}")
249
+
250
+ # keep the connection open by sending whitespace every 5 seconds
251
+ # leading whitespace will be ignored by json parsers
252
+ async def body() -> AsyncIterator[bytes]:
253
+ try:
224
254
  stream = asyncio.create_task(consume())
225
255
  while True:
226
256
  try:
@@ -235,15 +265,18 @@ async def wait_run(request: ApiRequest):
235
265
  stream.cancel()
236
266
  await stream
237
267
  raise
238
-
239
- return StreamingResponse(
240
- body(),
241
- media_type="application/json",
242
- headers={
243
- "Location": f"/threads/{thread_id}/runs/{run['run_id']}/join",
244
- "Content-Location": f"/threads/{thread_id}/runs/{run['run_id']}",
245
- },
246
- )
268
+ finally:
269
+ # Make sure to always clean up the pubsub
270
+ await sub.__aexit__(None, None, None)
271
+
272
+ return StreamingResponse(
273
+ body(),
274
+ media_type="application/json",
275
+ headers={
276
+ "Location": f"/threads/{thread_id}/runs/{run['run_id']}/join",
277
+ "Content-Location": f"/threads/{thread_id}/runs/{run['run_id']}",
278
+ },
279
+ )
247
280
 
248
281
 
249
282
  @retry_db
@@ -254,7 +287,9 @@ async def wait_run_stateless(request: ApiRequest):
254
287
  on_disconnect = payload.get("on_disconnect", "continue")
255
288
  run_id = uuid7()
256
289
  thread_id = uuid4()
257
- async with await Runs.Stream.subscribe(run_id, thread_id) as sub:
290
+
291
+ sub = await Runs.Stream.subscribe(run_id, thread_id)
292
+ try:
258
293
  async with connect() as conn:
259
294
  run = await create_valid_run(
260
295
  conn,
@@ -265,40 +300,41 @@ async def wait_run_stateless(request: ApiRequest):
265
300
  request_start_time=request.scope.get("request_start_time_ms"),
266
301
  temporary=True,
267
302
  )
268
-
269
- last_chunk = ValueEvent()
270
-
271
- async def consume():
272
- vchunk: bytes | None = None
273
- async for mode, chunk, _ in Runs.Stream.join(
274
- run["run_id"],
303
+ except Exception:
304
+ # Clean up the pubsub on errors
305
+ await sub.__aexit__(None, None, None)
306
+ raise
307
+
308
+ last_chunk = ValueEvent()
309
+
310
+ async def consume():
311
+ vchunk: bytes | None = None
312
+ async for mode, chunk, _ in Runs.Stream.join(
313
+ run["run_id"],
314
+ thread_id=run["thread_id"],
315
+ stream_channel=sub,
316
+ ignore_404=True,
317
+ cancel_on_disconnect=on_disconnect == "cancel",
318
+ ):
319
+ if mode == b"values" or mode == b"updates" and b"__interrupt__" in chunk:
320
+ vchunk = chunk
321
+ elif mode == b"error":
322
+ vchunk = orjson.dumps({"__error__": orjson.Fragment(chunk)})
323
+ if vchunk is not None:
324
+ last_chunk.set(vchunk)
325
+ else:
326
+ # we can't fetch the thread (it was deleted), so just return empty values
327
+ await logger.awarning(
328
+ "No checkpoint emitted for stateless run",
329
+ run_id=run["run_id"],
275
330
  thread_id=run["thread_id"],
276
- stream_channel=sub,
277
- ignore_404=True,
278
- cancel_on_disconnect=on_disconnect == "cancel",
279
- ):
280
- if (
281
- mode == b"values"
282
- or mode == b"updates"
283
- and b"__interrupt__" in chunk
284
- ):
285
- vchunk = chunk
286
- elif mode == b"error":
287
- vchunk = orjson.dumps({"__error__": orjson.Fragment(chunk)})
288
- if vchunk is not None:
289
- last_chunk.set(vchunk)
290
- else:
291
- # we can't fetch the thread (it was deleted), so just return empty values
292
- await logger.awarning(
293
- "No checkpoint emitted for stateless run",
294
- run_id=run["run_id"],
295
- thread_id=run["thread_id"],
296
- )
297
- last_chunk.set(b"{}")
298
-
299
- # keep the connection open by sending whitespace every 5 seconds
300
- # leading whitespace will be ignored by json parsers
301
- async def body() -> AsyncIterator[bytes]:
331
+ )
332
+ last_chunk.set(b"{}")
333
+
334
+ # keep the connection open by sending whitespace every 5 seconds
335
+ # leading whitespace will be ignored by json parsers
336
+ async def body() -> AsyncIterator[bytes]:
337
+ try:
302
338
  stream = asyncio.create_task(consume())
303
339
  while True:
304
340
  try:
@@ -313,15 +349,18 @@ async def wait_run_stateless(request: ApiRequest):
313
349
  stream.cancel("Run stream cancelled")
314
350
  await stream
315
351
  raise
316
-
317
- return StreamingResponse(
318
- body(),
319
- media_type="application/json",
320
- headers={
321
- "Location": f"/threads/{run['thread_id']}/runs/{run['run_id']}/join",
322
- "Content-Location": f"/threads/{run['thread_id']}/runs/{run['run_id']}",
323
- },
324
- )
352
+ finally:
353
+ # Make sure to always clean up the pubsub
354
+ await sub.__aexit__(None, None, None)
355
+
356
+ return StreamingResponse(
357
+ body(),
358
+ media_type="application/json",
359
+ headers={
360
+ "Location": f"/threads/{run['thread_id']}/runs/{run['run_id']}/join",
361
+ "Content-Location": f"/threads/{run['thread_id']}/runs/{run['run_id']}",
362
+ },
363
+ )
325
364
 
326
365
 
327
366
  @retry_db
@@ -402,14 +441,21 @@ async def join_run_stream(request: ApiRequest):
402
441
  validate_uuid(run_id, "Invalid run ID: must be a UUID")
403
442
  stream_mode = request.query_params.get("stream_mode") or []
404
443
  last_event_id = request.headers.get("last-event-id") or None
444
+
445
+ async def body():
446
+ async with await Runs.Stream.subscribe(run_id, thread_id) as sub:
447
+ async for event, message, stream_id in Runs.Stream.join(
448
+ run_id,
449
+ thread_id=thread_id,
450
+ cancel_on_disconnect=cancel_on_disconnect,
451
+ stream_channel=sub,
452
+ stream_mode=stream_mode,
453
+ last_event_id=last_event_id,
454
+ ):
455
+ yield event, message, stream_id
456
+
405
457
  return EventSourceResponse(
406
- Runs.Stream.join(
407
- run_id,
408
- thread_id=thread_id,
409
- cancel_on_disconnect=cancel_on_disconnect,
410
- stream_mode=stream_mode,
411
- last_event_id=last_event_id,
412
- ),
458
+ body(),
413
459
  )
414
460
 
415
461
 
@@ -31,6 +31,10 @@ class Fragment(NamedTuple):
31
31
  buf: bytes
32
32
 
33
33
 
34
+ def fragment_loads(data: bytes) -> Fragment:
35
+ return Fragment(data)
36
+
37
+
34
38
  def decimal_encoder(dec_value: Decimal) -> int | float:
35
39
  """
36
40
  Encodes a Decimal as int of there's no exponent, otherwise float
@@ -1 +0,0 @@
1
- __version__ = "0.4.18"
File without changes
File without changes
File without changes
File without changes