langgraph-api 0.2.40__tar.gz → 0.2.43__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 (92) hide show
  1. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/PKG-INFO +1 -1
  2. langgraph_api-0.2.43/langgraph_api/__init__.py +1 -0
  3. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/api/runs.py +7 -0
  4. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/logging.py +0 -18
  5. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/metadata.py +1 -12
  6. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/middleware/request_id.py +2 -0
  7. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/models/run.py +15 -2
  8. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/stream.py +6 -1
  9. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/worker.py +67 -20
  10. langgraph_api-0.2.40/langgraph_api/__init__.py +0 -1
  11. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/.gitignore +0 -0
  12. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/LICENSE +0 -0
  13. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/Makefile +0 -0
  14. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/README.md +0 -0
  15. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/benchmark/weather.js +0 -0
  16. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/constraints.txt +0 -0
  17. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/forbidden.txt +0 -0
  18. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/healthcheck.py +0 -0
  19. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/api/__init__.py +0 -0
  20. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/api/assistants.py +0 -0
  21. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/api/mcp.py +0 -0
  22. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/api/meta.py +0 -0
  23. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/api/openapi.py +0 -0
  24. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/api/store.py +0 -0
  25. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/api/threads.py +0 -0
  26. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/api/ui.py +0 -0
  27. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/asgi_transport.py +0 -0
  28. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/asyncio.py +0 -0
  29. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/auth/__init__.py +0 -0
  30. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/auth/custom.py +0 -0
  31. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/auth/langsmith/__init__.py +0 -0
  32. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/auth/langsmith/backend.py +0 -0
  33. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/auth/langsmith/client.py +0 -0
  34. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/auth/middleware.py +0 -0
  35. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/auth/noop.py +0 -0
  36. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/auth/studio_user.py +0 -0
  37. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/cli.py +0 -0
  38. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/command.py +0 -0
  39. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/config.py +0 -0
  40. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/cron_scheduler.py +0 -0
  41. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/errors.py +0 -0
  42. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/graph.py +0 -0
  43. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/http.py +0 -0
  44. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/.gitignore +0 -0
  45. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/.prettierrc +0 -0
  46. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/__init__.py +0 -0
  47. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/base.py +0 -0
  48. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/build.mts +0 -0
  49. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/client.http.mts +0 -0
  50. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/client.mts +0 -0
  51. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/errors.py +0 -0
  52. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/global.d.ts +0 -0
  53. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/package.json +0 -0
  54. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/remote.py +0 -0
  55. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/schema.py +0 -0
  56. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/src/graph.mts +0 -0
  57. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/src/load.hooks.mjs +0 -0
  58. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/src/preload.mjs +0 -0
  59. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/src/utils/files.mts +0 -0
  60. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/src/utils/importMap.mts +0 -0
  61. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/src/utils/pythonSchemas.mts +0 -0
  62. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/src/utils/serde.mts +0 -0
  63. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/sse.py +0 -0
  64. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/tsconfig.json +0 -0
  65. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/ui.py +0 -0
  66. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/js/yarn.lock +0 -0
  67. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/middleware/__init__.py +0 -0
  68. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/middleware/http_logger.py +0 -0
  69. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/middleware/private_network.py +0 -0
  70. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/models/__init__.py +0 -0
  71. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/patch.py +0 -0
  72. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/queue_entrypoint.py +0 -0
  73. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/route.py +0 -0
  74. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/schema.py +0 -0
  75. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/serde.py +0 -0
  76. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/server.py +0 -0
  77. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/sse.py +0 -0
  78. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/state.py +0 -0
  79. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/store.py +0 -0
  80. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/thread_ttl.py +0 -0
  81. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/tunneling/cloudflare.py +0 -0
  82. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/utils.py +0 -0
  83. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/validation.py +0 -0
  84. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_api/webhook.py +0 -0
  85. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_license/__init__.py +0 -0
  86. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_license/validation.py +0 -0
  87. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/langgraph_runtime/__init__.py +0 -0
  88. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/logging.json +0 -0
  89. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/openapi.json +0 -0
  90. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/pyproject.toml +0 -0
  91. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/scripts/create_license.py +0 -0
  92. {langgraph_api-0.2.40 → langgraph_api-0.2.43}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-api
3
- Version: 0.2.40
3
+ Version: 0.2.43
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.2.43"
@@ -38,6 +38,7 @@ async def create_run(request: ApiRequest):
38
38
  thread_id,
39
39
  payload,
40
40
  request.headers,
41
+ request_start_time=request.scope.get("request_start_time_ms"),
41
42
  )
42
43
  return ApiResponse(
43
44
  run,
@@ -55,6 +56,7 @@ async def create_stateless_run(request: ApiRequest):
55
56
  None,
56
57
  payload,
57
58
  request.headers,
59
+ request_start_time=request.scope.get("request_start_time_ms"),
58
60
  )
59
61
  return ApiResponse(
60
62
  run,
@@ -75,6 +77,7 @@ async def create_stateless_run_batch(request: ApiRequest):
75
77
  payload,
76
78
  request.headers,
77
79
  barrier,
80
+ request_start_time=request.scope.get("request_start_time_ms"),
78
81
  )
79
82
  for payload in batch_payload
80
83
  ]
@@ -100,6 +103,7 @@ async def stream_run(
100
103
  payload,
101
104
  request.headers,
102
105
  run_id=run_id,
106
+ request_start_time=request.scope.get("request_start_time_ms"),
103
107
  )
104
108
  except Exception:
105
109
  if not sub.cancelled():
@@ -139,6 +143,7 @@ async def stream_run_stateless(
139
143
  payload,
140
144
  request.headers,
141
145
  run_id=run_id,
146
+ request_start_time=request.scope.get("request_start_time_ms"),
142
147
  )
143
148
  except Exception:
144
149
  if not sub.cancelled():
@@ -178,6 +183,7 @@ async def wait_run(request: ApiRequest):
178
183
  payload,
179
184
  request.headers,
180
185
  run_id=run_id,
186
+ request_start_time=request.scope.get("request_start_time_ms"),
181
187
  )
182
188
  except Exception:
183
189
  if not sub.cancelled():
@@ -250,6 +256,7 @@ async def wait_run_stateless(request: ApiRequest):
250
256
  payload,
251
257
  request.headers,
252
258
  run_id=run_id,
259
+ request_start_time=request.scope.get("request_start_time_ms"),
253
260
  )
254
261
  except Exception:
255
262
  if not sub.cancelled():
@@ -81,23 +81,6 @@ class JSONRenderer:
81
81
  LEVELS = logging.getLevelNamesMapping()
82
82
 
83
83
 
84
- class TapForMetadata:
85
- def __call__(
86
- self, logger: logging.Logger, method_name: str, event_dict: EventDict
87
- ) -> str:
88
- """
89
- Tap WARN and above logs for metadata. Exclude user loggers.
90
- """
91
- if (
92
- event_dict["logger"].startswith("langgraph")
93
- and LEVELS[event_dict["level"].upper()] > LEVELS["INFO"]
94
- ):
95
- from langgraph_api.metadata import append_log
96
-
97
- append_log(event_dict.copy())
98
- return event_dict
99
-
100
-
101
84
  # shared config, for both logging and structlog
102
85
 
103
86
  shared_processors = [
@@ -136,7 +119,6 @@ class Formatter(structlog.stdlib.ProcessorFormatter):
136
119
  super().__init__(
137
120
  processors=[
138
121
  structlog.stdlib.ProcessorFormatter.remove_processors_meta,
139
- TapForMetadata(),
140
122
  renderer,
141
123
  ],
142
124
  foreign_pre_chain=shared_processors,
@@ -35,7 +35,6 @@ else:
35
35
  PLAN = "enterprise" if plus_features_enabled() else "developer"
36
36
  USER_API_URL = os.getenv("LANGGRAPH_API_URL", None)
37
37
 
38
- LOGS: list[dict] = []
39
38
  RUN_COUNTER = 0
40
39
  NODE_COUNTER = 0
41
40
  FROM_TIMESTAMP = datetime.now(UTC).isoformat()
@@ -59,14 +58,6 @@ def incr_nodes(_, *, incr: int = 1) -> None:
59
58
  NODE_COUNTER += incr
60
59
 
61
60
 
62
- def append_log(log: dict) -> None:
63
- if not LANGGRAPH_CLOUD_LICENSE_KEY and not LANGSMITH_API_KEY:
64
- return
65
-
66
- global LOGS
67
- LOGS.append(log)
68
-
69
-
70
61
  async def metadata_loop() -> None:
71
62
  try:
72
63
  from langgraph_api import __version__
@@ -91,8 +82,6 @@ async def metadata_loop() -> None:
91
82
  to_timestamp = datetime.now(UTC).isoformat()
92
83
  nodes = NODE_COUNTER
93
84
  runs = RUN_COUNTER
94
- logs = LOGS.copy()
95
- LOGS.clear()
96
85
  RUN_COUNTER = 0
97
86
  NODE_COUNTER = 0
98
87
  FROM_TIMESTAMP = to_timestamp
@@ -123,7 +112,7 @@ async def metadata_loop() -> None:
123
112
  "langgraph.platform.runs": runs,
124
113
  "langgraph.platform.nodes": nodes,
125
114
  },
126
- "logs": logs,
115
+ "logs": [],
127
116
  }
128
117
  try:
129
118
  await http_request(
@@ -1,6 +1,7 @@
1
1
  """Middleware to handle setting request IDs for logging."""
2
2
 
3
3
  import re
4
+ import time
4
5
  import uuid
5
6
 
6
7
  from starlette.types import ASGIApp, Receive, Scope, Send
@@ -27,4 +28,5 @@ class RequestIdMiddleware:
27
28
  if request_id is None:
28
29
  request_id = str(uuid.uuid4()).encode()
29
30
  scope["headers"].append((b"x-request-id", request_id))
31
+ scope["request_start_time_ms"] = int(time.time() * 1000)
30
32
  await self.app(scope, receive, send)
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import functools
3
3
  import re
4
+ import time
4
5
  import urllib.parse
5
6
  import uuid
6
7
  from collections.abc import Mapping, Sequence
@@ -249,6 +250,7 @@ async def create_valid_run(
249
250
  headers: Mapping[str, str],
250
251
  barrier: asyncio.Barrier | None = None,
251
252
  run_id: UUID | None = None,
253
+ request_start_time: float | None = None,
252
254
  ) -> Run:
253
255
  request_id = headers.get("x-request-id") # Will be null in the crons scheduler.
254
256
  (
@@ -293,6 +295,11 @@ async def create_valid_run(
293
295
  user_id = None
294
296
  if not configurable.get("langgraph_request_id"):
295
297
  configurable["langgraph_request_id"] = request_id
298
+ if request_start_time:
299
+ configurable["__request_start_time_ms__"] = request_start_time
300
+ after_seconds = payload.get("after_seconds", 0)
301
+ configurable["__after_seconds__"] = after_seconds
302
+ put_time_start = time.time()
296
303
  run_coro = Runs.put(
297
304
  conn,
298
305
  assistant_id,
@@ -317,7 +324,7 @@ async def create_valid_run(
317
324
  run_id=run_id,
318
325
  multitask_strategy=multitask_strategy,
319
326
  prevent_insert_if_inflight=prevent_insert_if_inflight,
320
- after_seconds=payload.get("after_seconds", 0),
327
+ after_seconds=after_seconds,
321
328
  if_not_exists=payload.get("if_not_exists", "reject"),
322
329
  )
323
330
  run_ = await run_coro
@@ -344,8 +351,14 @@ async def create_valid_run(
344
351
  multitask_strategy=multitask_strategy,
345
352
  stream_mode=stream_mode,
346
353
  temporary=temporary,
347
- after_seconds=payload.get("after_seconds", 0),
354
+ after_seconds=after_seconds,
348
355
  if_not_exists=payload.get("if_not_exists", "reject"),
356
+ run_create_ms=(
357
+ int(time.time() * 1_000) - request_start_time
358
+ if request_start_time
359
+ else None
360
+ ),
361
+ run_put_ms=int((time.time() - put_time_start) * 1_000),
349
362
  )
350
363
  # inserted, proceed
351
364
  if multitask_strategy in ("interrupt", "rollback") and inflight_runs:
@@ -207,11 +207,16 @@ async def astream_state(
207
207
  elif "events" in stream_mode:
208
208
  yield "events", event
209
209
  else:
210
+ output_keys = kwargs.pop("output_keys", graph.output_channels)
210
211
  async with (
211
212
  stack,
212
213
  aclosing(
213
214
  graph.astream(
214
- input, config, stream_mode=list(stream_modes_set), **kwargs
215
+ input,
216
+ config,
217
+ stream_mode=list(stream_modes_set),
218
+ output_keys=output_keys,
219
+ **kwargs,
215
220
  )
216
221
  ) as stream,
217
222
  ):
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import time
2
3
  from collections.abc import AsyncGenerator
3
4
  from contextlib import AsyncExitStack, asynccontextmanager
4
5
  from datetime import UTC, datetime
@@ -74,8 +75,21 @@ async def worker(
74
75
  exception: Exception | None = None
75
76
  status: str | None = None
76
77
  webhook = run["kwargs"].get("webhook", None)
77
- run_started_at = datetime.now(UTC)
78
+ request_created_at: int | None = run["kwargs"]["config"]["configurable"].get(
79
+ "__request_start_time_ms__"
80
+ )
81
+ after_seconds = run["kwargs"]["config"]["configurable"].get("__after_seconds__", 0)
78
82
  run_ended_at: str | None = None
83
+ run_started_at = datetime.now(UTC)
84
+ # Note that "created_at" is inclusive of the `after_seconds`
85
+ run_creation_ms = (
86
+ int(
87
+ ((run["created_at"].timestamp() - after_seconds) * 1_000)
88
+ - request_created_at
89
+ )
90
+ if request_created_at is not None
91
+ else None
92
+ )
79
93
 
80
94
  async with (
81
95
  connect() as conn,
@@ -95,11 +109,13 @@ async def worker(
95
109
  "request_id": _get_request_id(run),
96
110
  }
97
111
  )
98
-
112
+ run_stream_started_at = datetime.now(UTC)
99
113
  await logger.ainfo(
100
114
  "Starting background run",
101
115
  run_started_at=run_started_at.isoformat(),
116
+ run_creation_ms=run_creation_ms,
102
117
  run_queue_ms=ms(run_started_at, run["created_at"]),
118
+ run_stream_start_ms=ms(run_stream_started_at, run_started_at),
103
119
  )
104
120
 
105
121
  def on_checkpoint(checkpoint_arg: CheckpointPayload):
@@ -115,7 +131,15 @@ async def worker(
115
131
 
116
132
  try:
117
133
  if attempt > BG_JOB_MAX_RETRIES:
118
- await logger.aerror("Run exceeded max attempts", run_id=run["run_id"])
134
+ await logger.aerror(
135
+ "Run exceeded max attempts",
136
+ run_id=run["run_id"],
137
+ run_completed_in_ms=(
138
+ int((time.time() * 1_000) - request_created_at)
139
+ if request_created_at is not None
140
+ else None
141
+ ),
142
+ )
119
143
 
120
144
  error_message = (
121
145
  f"Run {run['run_id']} exceeded max attempts ({BG_JOB_MAX_RETRIES}).\n\n"
@@ -131,13 +155,12 @@ async def worker(
131
155
  )
132
156
 
133
157
  raise RuntimeError(error_message)
158
+ exit_stack = AsyncExitStack()
134
159
  if temporary:
135
- stream = astream_state(
136
- AsyncExitStack(), conn, cast(Run, run), attempt, done
137
- )
160
+ stream = astream_state(exit_stack, conn, cast(Run, run), attempt, done)
138
161
  else:
139
162
  stream = astream_state(
140
- AsyncExitStack(),
163
+ exit_stack,
141
164
  conn,
142
165
  cast(Run, run),
143
166
  attempt,
@@ -149,7 +172,8 @@ async def worker(
149
172
  consume(stream, run_id, resumable),
150
173
  BG_JOB_TIMEOUT_SECS,
151
174
  )
152
- run_ended_at = datetime.now(UTC).isoformat()
175
+ run_ended_at_dt = datetime.now(UTC)
176
+ run_ended_at = run_ended_at_dt.isoformat()
153
177
  await logger.ainfo(
154
178
  "Background run succeeded",
155
179
  run_id=str(run_id),
@@ -157,7 +181,12 @@ async def worker(
157
181
  run_created_at=run_created_at,
158
182
  run_started_at=run_started_at.isoformat(),
159
183
  run_ended_at=run_ended_at,
160
- run_exec_ms=ms(datetime.now(UTC), run_started_at),
184
+ run_exec_ms=ms(run_ended_at_dt, run_started_at),
185
+ run_completed_in_ms=(
186
+ int((run_ended_at_dt.timestamp() * 1_000) - request_created_at)
187
+ if request_created_at is not None
188
+ else None
189
+ ),
161
190
  )
162
191
  status = "success"
163
192
  await Runs.set_status(conn, run_id, "success")
@@ -173,13 +202,21 @@ async def worker(
173
202
  run_started_at=run_started_at.isoformat(),
174
203
  run_ended_at=run_ended_at,
175
204
  run_exec_ms=ms(datetime.now(UTC), run_started_at),
205
+ run_completed_in_ms=(
206
+ int((run_ended_at_dt.timestamp() * 1_000) - request_created_at)
207
+ if request_created_at is not None
208
+ else None
209
+ ),
176
210
  )
177
211
  await Runs.set_status(conn, run_id, "timeout")
178
212
  except UserRollback as e:
179
213
  exception = e
180
214
  status = "rollback"
181
- run_ended_at = datetime.now(UTC).isoformat()
215
+ run_ended_at_dt = datetime.now(UTC)
216
+ run_ended_at = run_ended_at_dt.isoformat()
182
217
  try:
218
+ # Need to close the pipeline to re-use the connection
219
+ await exit_stack.aclose()
183
220
  await Runs.delete(conn, run_id, thread_id=run["thread_id"])
184
221
  await logger.ainfo(
185
222
  "Background run rolled back",
@@ -188,7 +225,12 @@ async def worker(
188
225
  run_created_at=run_created_at,
189
226
  run_started_at=run_started_at.isoformat(),
190
227
  run_ended_at=run_ended_at,
191
- run_exec_ms=ms(datetime.now(UTC), run_started_at),
228
+ run_exec_ms=ms(run_ended_at_dt, run_started_at),
229
+ run_completed_in_ms=(
230
+ int((run_ended_at_dt.timestamp() * 1_000) - request_created_at)
231
+ if request_created_at is not None
232
+ else None
233
+ ),
192
234
  )
193
235
 
194
236
  except InFailedSqlTransaction as e:
@@ -199,9 +241,6 @@ async def worker(
199
241
  run_created_at=run_created_at,
200
242
  exc=str(e),
201
243
  )
202
- # We need to clean up the transaction early if we want to
203
- # update the thread status with the same connection
204
- await exit.aclose()
205
244
  except HTTPException as e:
206
245
  if e.status_code == 404:
207
246
  await logger.ainfo(
@@ -217,7 +256,8 @@ async def worker(
217
256
  except UserInterrupt as e:
218
257
  exception = e
219
258
  status = "interrupted"
220
- run_ended_at = datetime.now(UTC).isoformat()
259
+ run_ended_at_dt = datetime.now(UTC)
260
+ run_ended_at = run_ended_at_dt.isoformat()
221
261
  await logger.ainfo(
222
262
  "Background run interrupted",
223
263
  run_id=str(run_id),
@@ -225,13 +265,19 @@ async def worker(
225
265
  run_created_at=run_created_at,
226
266
  run_started_at=run_started_at.isoformat(),
227
267
  run_ended_at=run_ended_at,
228
- run_exec_ms=ms(datetime.now(UTC), run_started_at),
268
+ run_exec_ms=ms(run_ended_at_dt, run_started_at),
269
+ run_completed_in_ms=(
270
+ int((run_ended_at_dt.timestamp() * 1_000) - request_created_at)
271
+ if request_created_at is not None
272
+ else None
273
+ ),
229
274
  )
230
275
  await Runs.set_status(conn, run_id, "interrupted")
231
276
  except RETRIABLE_EXCEPTIONS as e:
232
277
  exception = e
233
278
  status = "retry"
234
- run_ended_at = datetime.now(UTC).isoformat()
279
+ run_ended_at_dt = datetime.now(UTC)
280
+ run_ended_at = run_ended_at_dt.isoformat()
235
281
  await logger.awarning(
236
282
  f"Background run failed, will retry. Exception: {e}",
237
283
  exc_info=True,
@@ -240,14 +286,15 @@ async def worker(
240
286
  run_created_at=run_created_at,
241
287
  run_started_at=run_started_at.isoformat(),
242
288
  run_ended_at=run_ended_at,
243
- run_exec_ms=ms(datetime.now(UTC), run_started_at),
289
+ run_exec_ms=ms(run_ended_at_dt, run_started_at),
244
290
  )
245
291
  await Runs.set_status(conn, run_id, "pending")
246
292
  raise
247
293
  except Exception as exc:
248
294
  exception = exc
249
295
  status = "error"
250
- run_ended_at = datetime.now(UTC).isoformat()
296
+ run_ended_at_dt = datetime.now(UTC)
297
+ run_ended_at = run_ended_at_dt.isoformat()
251
298
  await logger.aexception(
252
299
  f"Background run failed. Exception: {exc}",
253
300
  exc_info=not isinstance(exc, RemoteException),
@@ -256,7 +303,7 @@ async def worker(
256
303
  run_created_at=run_created_at,
257
304
  run_started_at=run_started_at.isoformat(),
258
305
  run_ended_at=run_ended_at,
259
- run_exec_ms=ms(datetime.now(UTC), run_started_at),
306
+ run_exec_ms=ms(run_ended_at_dt, run_started_at),
260
307
  )
261
308
  await Runs.set_status(conn, run_id, "error")
262
309
  set_auth_ctx(None, None)
@@ -1 +0,0 @@
1
- __version__ = "0.2.40"
File without changes
File without changes
File without changes
File without changes