langgraph-api 0.18.0.dev1__py3-none-any.whl → 0.18.0.dev3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. langgraph_api/__init__.py +1 -1
  2. langgraph_api/api/a2a.py +93 -10
  3. langgraph_api/api/event_streaming.py +10 -2
  4. langgraph_api/api/mcp/_calls.py +29 -0
  5. langgraph_api/api/mcp/_constants.py +15 -6
  6. langgraph_api/api/mcp/_handlers.py +146 -196
  7. langgraph_api/api/mcp/_outcome.py +43 -0
  8. langgraph_api/api/mcp/_protocol.py +224 -0
  9. langgraph_api/config/__init__.py +37 -1
  10. langgraph_api/cron_scheduler.py +2 -2
  11. langgraph_api/drain.py +102 -0
  12. langgraph_api/event_streaming/service.py +77 -26
  13. langgraph_api/executor/__init__.py +9 -0
  14. langgraph_api/executor/__main__.py +343 -0
  15. langgraph_api/feature_flags.py +6 -0
  16. langgraph_api/graph.py +12 -2
  17. langgraph_api/grpc/ops/runs.py +41 -5
  18. langgraph_api/metrics_otlp.py +8 -0
  19. langgraph_api/models/run.py +4 -3
  20. langgraph_api/queue_entrypoint.py +13 -1
  21. langgraph_api/self_hosted_logs.py +3 -1
  22. langgraph_api/self_hosted_metrics.py +1 -1
  23. langgraph_api/stream.py +49 -3
  24. langgraph_api/timing/__init__.py +16 -0
  25. langgraph_api/timing/startup.py +184 -0
  26. langgraph_api/timing/timer.py +16 -2
  27. langgraph_api/utils/headers.py +15 -3
  28. langgraph_api/validation.py +6 -0
  29. langgraph_api/worker.py +146 -13
  30. langgraph_api/worker_runtime.py +124 -4
  31. {langgraph_api-0.18.0.dev1.dist-info → langgraph_api-0.18.0.dev3.dist-info}/METADATA +1 -1
  32. {langgraph_api-0.18.0.dev1.dist-info → langgraph_api-0.18.0.dev3.dist-info}/RECORD +40 -35
  33. langgraph_grpc_common/proto/__init__.py +4 -4
  34. langgraph_grpc_common/proto/orchestrator_pb2.py +50 -0
  35. langgraph_grpc_common/proto/{runner_pb2.pyi → orchestrator_pb2.pyi} +4 -4
  36. langgraph_grpc_common/proto/orchestrator_pb2_grpc.py +108 -0
  37. langgraph_grpc_common/proto/orchestrator_pb2_grpc.pyi +82 -0
  38. langgraph_api/runner/__init__.py +0 -10
  39. langgraph_api/runner/__main__.py +0 -275
  40. langgraph_grpc_common/proto/runner_pb2.py +0 -50
  41. langgraph_grpc_common/proto/runner_pb2_grpc.py +0 -108
  42. langgraph_grpc_common/proto/runner_pb2_grpc.pyi +0 -73
  43. {langgraph_api-0.18.0.dev1.dist-info → langgraph_api-0.18.0.dev3.dist-info}/WHEEL +0 -0
  44. {langgraph_api-0.18.0.dev1.dist-info → langgraph_api-0.18.0.dev3.dist-info}/entry_points.txt +0 -0
  45. {langgraph_api-0.18.0.dev1.dist-info → langgraph_api-0.18.0.dev3.dist-info}/licenses/LICENSE +0 -0
langgraph_api/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.18.0.dev1"
1
+ __version__ = "0.18.0.dev3"
langgraph_api/api/a2a.py CHANGED
@@ -2090,6 +2090,19 @@ async def handle_message_send(
2090
2090
  "message": "Invalid params: 'configuration' must be an object",
2091
2091
  }
2092
2092
  }
2093
+ for field in ("returnImmediately", "blocking"):
2094
+ if configuration.get(field) is not None and not isinstance(
2095
+ configuration[field], bool
2096
+ ):
2097
+ return {
2098
+ "error": {
2099
+ "code": ERROR_CODE_INVALID_PARAMS,
2100
+ "message": f"Invalid params: '{field}' must be a boolean",
2101
+ }
2102
+ }
2103
+ return_immediately = configuration.get("returnImmediately")
2104
+ if return_immediately is None:
2105
+ return_immediately = configuration.get("blocking") is False
2093
2106
  history_length = configuration.get("historyLength")
2094
2107
  if history_error := _validate_history_length(history_length):
2095
2108
  return {
@@ -2308,6 +2321,36 @@ async def handle_message_send(
2308
2321
  context_id = str(run["thread_id"])
2309
2322
  if task_id is None:
2310
2323
  task_id = _make_task_id(context_id, str(run["run_id"]))
2324
+ if return_immediately:
2325
+ immediate_task: dict[str, Any] = {
2326
+ "kind": "task",
2327
+ "id": task_id,
2328
+ "contextId": context_id,
2329
+ "status": {
2330
+ "state": _task_state_for_run(run),
2331
+ "timestamp": _run_timestamp(run),
2332
+ },
2333
+ }
2334
+ if history_length is not None and history_length != 0:
2335
+ run_messages = await _load_run_messages(
2336
+ context_id, str(run["run_id"]), request.headers
2337
+ )
2338
+ if not run_messages.messages:
2339
+ run_messages = RunMessages(input_content.get("messages", []), 0)
2340
+ if task_start_id:
2341
+ task_ids_by_start_id.setdefault(task_start_id, task_id)
2342
+ immediate_task["history"] = _convert_messages_to_a2a_format(
2343
+ run_messages.stamped(task_id),
2344
+ task_id,
2345
+ context_id,
2346
+ history_length=history_length,
2347
+ history_scope=history_scope,
2348
+ a2ui=a2ui,
2349
+ task_ids_by_start_id=task_ids_by_start_id,
2350
+ )
2351
+ return {"result": {"task": immediate_task}}
2352
+
2353
+ if existing_task_id is None:
2311
2354
  try:
2312
2355
  runs = await _list_context_runs(
2313
2356
  client, context_id=context_id, headers=request.headers
@@ -2489,6 +2532,48 @@ async def handle_tasks_get(
2489
2532
  lg_status = run_info.get("status", "unknown")
2490
2533
  a2a_state = task.state
2491
2534
 
2535
+ if a2a_state in ("TASK_STATE_COMPLETED", "TASK_STATE_INPUT_REQUIRED"):
2536
+ states, before = await asyncio.gather(
2537
+ get_client().threads.get_history(
2538
+ context_id,
2539
+ limit=1,
2540
+ metadata={"run_id": str(run_info["run_id"])},
2541
+ headers=request.headers,
2542
+ ),
2543
+ _latest_messages(
2544
+ context_id,
2545
+ {"run_id": str(run_info["run_id"]), "source": "input"},
2546
+ request.headers,
2547
+ ),
2548
+ )
2549
+ result = dict(states[0]["values"]) if states else {}
2550
+ messages = result.get("messages", []) or []
2551
+ run_messages = (
2552
+ RunMessages.between(before, messages)
2553
+ if before is not None
2554
+ else RunMessages(messages, None)
2555
+ )
2556
+ if "messages" in result:
2557
+ result["messages"] = run_messages.stamped(task_id)
2558
+ if a2a_state == "TASK_STATE_INPUT_REQUIRED":
2559
+ result["__interrupt__"] = (
2560
+ states[0].get("interrupts", []) if states else []
2561
+ )
2562
+ response = await _create_task_response(
2563
+ task_id=task_id,
2564
+ context_id=context_id,
2565
+ result=result,
2566
+ assistant_id=assistant_id,
2567
+ a2ui=_a2ui_context_for_run(run_info),
2568
+ history_length=history_length,
2569
+ history_scope=history_scope,
2570
+ task_ids_by_start_id=task.task_ids_by_start_id,
2571
+ turn=run_messages.added,
2572
+ )
2573
+ task_response = response["result"]["task"]
2574
+ task_response["status"]["timestamp"] = task.status_timestamp
2575
+ return {"result": task_response}
2576
+
2492
2577
  thread_history = []
2493
2578
  if history_length != 0:
2494
2579
  try:
@@ -2532,16 +2617,7 @@ async def handle_tasks_get(
2532
2617
  if history_length != 0:
2533
2618
  task_response["history"] = thread_history
2534
2619
 
2535
- # Add result message if completed
2536
- if a2a_state == "TASK_STATE_COMPLETED":
2537
- task_response["status"]["message"] = {
2538
- "kind": "message",
2539
- "role": "ROLE_AGENT",
2540
- "parts": [{"kind": "text", "text": "Task completed successfully"}],
2541
- "messageId": str(uuid.uuid4()),
2542
- "taskId": task_id,
2543
- }
2544
- elif a2a_state == "TASK_STATE_FAILED":
2620
+ if a2a_state == "TASK_STATE_FAILED":
2545
2621
  task_response["status"]["message"] = {
2546
2622
  "kind": "message",
2547
2623
  "role": "ROLE_AGENT",
@@ -2554,6 +2630,13 @@ async def handle_tasks_get(
2554
2630
 
2555
2631
  return {"result": task_response}
2556
2632
 
2633
+ except InvalidAgentResponseError as e:
2634
+ return {
2635
+ "error": {
2636
+ "code": ERROR_CODE_INVALID_AGENT_RESPONSE,
2637
+ "message": str(e),
2638
+ }
2639
+ }
2557
2640
  except Exception as e:
2558
2641
  await logger.aexception(
2559
2642
  f"Error in tasks/get for task {params.get('id')}: {e!s}", exc_info=True
@@ -40,6 +40,8 @@ from langgraph_api.serde import json_dumpb
40
40
  from langgraph_api.sse import EventSourceResponse
41
41
 
42
42
  if TYPE_CHECKING:
43
+ from collections.abc import Mapping
44
+
43
45
  from starlette.requests import Request
44
46
  from starlette.websockets import WebSocket
45
47
 
@@ -91,7 +93,12 @@ def _json_response(content: Any, *, status_code: int = 200) -> Response:
91
93
  )
92
94
 
93
95
 
94
- def _make_manager(thread_id: str, send_event: Any = None) -> ThreadRunManager:
96
+ def _make_manager(
97
+ thread_id: str,
98
+ send_event: Any = None,
99
+ *,
100
+ headers: Mapping[str, str] | None = None,
101
+ ) -> ThreadRunManager:
95
102
  # Auth context is propagated via ``ApiRoute``'s ``with_user`` wrapper
96
103
  # — ops reads it off the context var, so we don't thread it through
97
104
  # the manager explicitly.
@@ -100,6 +107,7 @@ def _make_manager(thread_id: str, send_event: Any = None) -> ThreadRunManager:
100
107
  runs=Runs,
101
108
  threads=Threads,
102
109
  send_event=send_event,
110
+ headers=headers,
103
111
  )
104
112
 
105
113
 
@@ -353,7 +361,7 @@ async def _thread_command(request: Request) -> Response:
353
361
  status_code=400,
354
362
  )
355
363
 
356
- manager = _make_manager(thread_id)
364
+ manager = _make_manager(thread_id, headers=request.headers)
357
365
  try:
358
366
  resp = await manager.handle_command(payload)
359
367
  return _json_response(resp)
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from langgraph_api.api.mcp._protocol import ProtocolError
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Mapping
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class ToolCall:
14
+ name: str
15
+ arguments: dict[str, Any]
16
+ context: dict[str, Any] | None
17
+
18
+ @classmethod
19
+ def of(cls, params: Mapping[str, Any]) -> ToolCall:
20
+ name = params.get("name")
21
+ arguments = params.get("arguments", {})
22
+ context = params.get("context")
23
+ if not isinstance(name, str) or not name:
24
+ raise ProtocolError(f"Unknown tool: {name}")
25
+ if not isinstance(arguments, dict) or (
26
+ context is not None and not isinstance(context, dict)
27
+ ):
28
+ raise ProtocolError("arguments and context must be objects")
29
+ return cls(name, arguments, context)
@@ -1,14 +1,15 @@
1
- # Workaround assistant name not exposed in the Assistants.search API
2
1
  MAX_ASSISTANTS = 1000
3
2
  DEFAULT_PAGE_SIZE = 100
3
+ MAX_PAGE_OFFSET = 2**63 - 1
4
+ MAX_CURSOR_LENGTH = len(str(MAX_PAGE_OFFSET))
4
5
 
5
- # JSON-RPC error codes: https://www.jsonrpc.org/specification#error_object
6
+ ERROR_CODE_INVALID_REQUEST = -32600
6
7
  ERROR_CODE_INVALID_PARAMS = -32602
7
8
  ERROR_CODE_METHOD_NOT_FOUND = -32601
9
+ ERROR_CODE_HEADER_MISMATCH = -32020
10
+ ERROR_CODE_UNSUPPORTED_VERSION = -32022
8
11
 
9
- # Supported MCP protocol versions
10
- # https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle
11
- SUPPORTED_PROTOCOL_VERSIONS = frozenset(
12
+ LEGACY_PROTOCOL_VERSIONS = frozenset(
12
13
  (
13
14
  "2024-11-05",
14
15
  "2025-03-26",
@@ -16,4 +17,12 @@ SUPPORTED_PROTOCOL_VERSIONS = frozenset(
16
17
  "2025-11-25",
17
18
  )
18
19
  )
19
- LATEST_PROTOCOL_VERSION = "2025-11-25"
20
+ LATEST_LEGACY_PROTOCOL_VERSION = "2025-11-25"
21
+ PROTOCOL_VERSION = "2026-07-28"
22
+ SUPPORTED_PROTOCOL_VERSIONS = LEGACY_PROTOCOL_VERSIONS | {PROTOCOL_VERSION}
23
+
24
+ PAUSED_TOOL_MESSAGE = (
25
+ "This tool paused to ask for human input, and this client can't answer it."
26
+ )
27
+ RUN_FAILED_MESSAGE = "The tool's run failed."
28
+ INVALID_CURSOR_MESSAGE = "Invalid cursor"