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.
- langgraph_api/__init__.py +1 -1
- langgraph_api/api/a2a.py +93 -10
- langgraph_api/api/event_streaming.py +10 -2
- langgraph_api/api/mcp/_calls.py +29 -0
- langgraph_api/api/mcp/_constants.py +15 -6
- langgraph_api/api/mcp/_handlers.py +146 -196
- langgraph_api/api/mcp/_outcome.py +43 -0
- langgraph_api/api/mcp/_protocol.py +224 -0
- langgraph_api/config/__init__.py +37 -1
- langgraph_api/cron_scheduler.py +2 -2
- langgraph_api/drain.py +102 -0
- langgraph_api/event_streaming/service.py +77 -26
- langgraph_api/executor/__init__.py +9 -0
- langgraph_api/executor/__main__.py +343 -0
- langgraph_api/feature_flags.py +6 -0
- langgraph_api/graph.py +12 -2
- langgraph_api/grpc/ops/runs.py +41 -5
- langgraph_api/metrics_otlp.py +8 -0
- langgraph_api/models/run.py +4 -3
- langgraph_api/queue_entrypoint.py +13 -1
- langgraph_api/self_hosted_logs.py +3 -1
- langgraph_api/self_hosted_metrics.py +1 -1
- langgraph_api/stream.py +49 -3
- langgraph_api/timing/__init__.py +16 -0
- langgraph_api/timing/startup.py +184 -0
- langgraph_api/timing/timer.py +16 -2
- langgraph_api/utils/headers.py +15 -3
- langgraph_api/validation.py +6 -0
- langgraph_api/worker.py +146 -13
- langgraph_api/worker_runtime.py +124 -4
- {langgraph_api-0.18.0.dev1.dist-info → langgraph_api-0.18.0.dev3.dist-info}/METADATA +1 -1
- {langgraph_api-0.18.0.dev1.dist-info → langgraph_api-0.18.0.dev3.dist-info}/RECORD +40 -35
- langgraph_grpc_common/proto/__init__.py +4 -4
- langgraph_grpc_common/proto/orchestrator_pb2.py +50 -0
- langgraph_grpc_common/proto/{runner_pb2.pyi → orchestrator_pb2.pyi} +4 -4
- langgraph_grpc_common/proto/orchestrator_pb2_grpc.py +108 -0
- langgraph_grpc_common/proto/orchestrator_pb2_grpc.pyi +82 -0
- langgraph_api/runner/__init__.py +0 -10
- langgraph_api/runner/__main__.py +0 -275
- langgraph_grpc_common/proto/runner_pb2.py +0 -50
- langgraph_grpc_common/proto/runner_pb2_grpc.py +0 -108
- langgraph_grpc_common/proto/runner_pb2_grpc.pyi +0 -73
- {langgraph_api-0.18.0.dev1.dist-info → langgraph_api-0.18.0.dev3.dist-info}/WHEEL +0 -0
- {langgraph_api-0.18.0.dev1.dist-info → langgraph_api-0.18.0.dev3.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|