agentkit-sdk-python 0.7.10__py3-none-any.whl → 0.7.12__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.
agentkit/apps/__init__.py CHANGED
@@ -17,6 +17,9 @@ from typing import TYPE_CHECKING
17
17
  if TYPE_CHECKING:
18
18
  from agentkit.apps.a2a_app.a2a_app import AgentkitA2aApp
19
19
  from agentkit.apps.agent_server_app.agent_server_app import AgentkitAgentServerApp
20
+ from agentkit.apps.langgraph_server_app.langgraph_server_app import (
21
+ AgentkitLangGraphServerApp,
22
+ )
20
23
  from agentkit.apps.mcp_app.mcp_app import AgentkitMCPApp
21
24
  from agentkit.apps.simple_app.simple_app import AgentkitSimpleApp
22
25
 
@@ -28,6 +31,7 @@ def __getattr__(
28
31
  | type["AgentkitMCPApp"]
29
32
  | type["AgentkitSimpleApp"]
30
33
  | type["AgentkitAgentServerApp"]
34
+ | type["AgentkitLangGraphServerApp"]
31
35
  ):
32
36
  if name == "AgentkitA2aApp":
33
37
  from agentkit.apps.a2a_app.a2a_app import AgentkitA2aApp
@@ -47,6 +51,12 @@ def __getattr__(
47
51
  )
48
52
 
49
53
  return AgentkitAgentServerApp
54
+ if name == "AgentkitLangGraphServerApp":
55
+ from agentkit.apps.langgraph_server_app.langgraph_server_app import (
56
+ AgentkitLangGraphServerApp,
57
+ )
58
+
59
+ return AgentkitLangGraphServerApp
50
60
  raise AttributeError(f"module 'agentkit.apps' has no attribute '{name}'")
51
61
 
52
62
 
@@ -55,4 +65,5 @@ __all__ = [
55
65
  "AgentkitMCPApp",
56
66
  "AgentkitSimpleApp",
57
67
  "AgentkitAgentServerApp",
68
+ "AgentkitLangGraphServerApp",
58
69
  ]
@@ -39,14 +39,22 @@ from google.adk.evaluation.local_eval_set_results_manager import (
39
39
  )
40
40
  from google.adk.evaluation.local_eval_sets_manager import LocalEvalSetsManager
41
41
  from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
42
+ from google.adk.runners import Runner
42
43
  from google.adk.sessions.base_session_service import BaseSessionService
44
+ from google.adk.sessions.in_memory_session_service import InMemorySessionService
43
45
  from google.adk.utils.context_utils import Aclosing
44
46
  from google.genai import types
45
47
  from opentelemetry import trace
46
48
  from typing_extensions import override
47
- from veadk import Agent
48
- from veadk.memory.short_term_memory import ShortTermMemory
49
- from veadk.runner import Runner
49
+
50
+ try: # pragma: no cover - exercised when users install veadk explicitly.
51
+ from veadk import Agent as VeadkAgent
52
+ from veadk.memory.short_term_memory import ShortTermMemory
53
+ from veadk.runner import Runner as VeadkRunner
54
+ except ImportError: # pragma: no cover - default migration path avoids veadk.
55
+ VeadkAgent = None # type: ignore[assignment]
56
+ ShortTermMemory = None # type: ignore[assignment,misc]
57
+ VeadkRunner = None # type: ignore[assignment]
50
58
 
51
59
  from agentkit.apps.agent_server_app.middleware import (
52
60
  AgentkitTelemetryHTTPMiddleware,
@@ -64,6 +72,65 @@ from agentkit.apps.base_app import BaseAgentkitApp
64
72
  logger = logging.getLogger(__name__)
65
73
 
66
74
 
75
+ def _is_veadk_short_term_memory(value: Any) -> bool:
76
+ return ShortTermMemory is not None and isinstance(value, ShortTermMemory)
77
+
78
+
79
+ def _is_veadk_agent(value: Any) -> bool:
80
+ return VeadkAgent is not None and isinstance(value, VeadkAgent)
81
+
82
+
83
+ def _resolve_session_service(
84
+ short_term_memory: BaseSessionService | Any | None,
85
+ ) -> BaseSessionService:
86
+ if short_term_memory is None:
87
+ return InMemorySessionService()
88
+ if isinstance(short_term_memory, BaseSessionService):
89
+ return short_term_memory
90
+ if _is_veadk_short_term_memory(short_term_memory):
91
+ return short_term_memory.session_service
92
+ raise TypeError(
93
+ "short_term_memory must be a google.adk BaseSessionService, "
94
+ "a veadk ShortTermMemory, or None."
95
+ )
96
+
97
+
98
+ def _resolve_memory_service(root_agent: BaseAgent) -> Any:
99
+ if _is_veadk_agent(root_agent):
100
+ long_term_memory = getattr(root_agent, "long_term_memory", None)
101
+ if long_term_memory:
102
+ return long_term_memory
103
+ return InMemoryMemoryService()
104
+
105
+
106
+ def _create_a2a_runner(
107
+ *,
108
+ root_agent: BaseAgent,
109
+ short_term_memory: BaseSessionService | Any | None,
110
+ session_service: BaseSessionService,
111
+ memory_service: Any,
112
+ artifact_service: InMemoryArtifactService,
113
+ credential_service: InMemoryCredentialService,
114
+ ) -> Any:
115
+ if VeadkRunner is not None and (
116
+ _is_veadk_agent(root_agent) or _is_veadk_short_term_memory(short_term_memory)
117
+ ):
118
+ return VeadkRunner(
119
+ agent=root_agent,
120
+ short_term_memory=short_term_memory
121
+ if _is_veadk_short_term_memory(short_term_memory)
122
+ else None,
123
+ )
124
+ return Runner(
125
+ agent=root_agent,
126
+ app_name=root_agent.name,
127
+ session_service=session_service,
128
+ memory_service=memory_service,
129
+ artifact_service=artifact_service,
130
+ credential_service=credential_service,
131
+ )
132
+
133
+
67
134
  async def _call_lifecycle_handler(handler: Any) -> None:
68
135
  result = handler()
69
136
  if inspect.isawaitable(result):
@@ -143,9 +210,6 @@ class AgentkitAgentServerApp(BaseAgentkitApp):
143
210
  ) -> None:
144
211
  super().__init__()
145
212
 
146
- if short_term_memory is None:
147
- raise TypeError("short_term_memory is required.")
148
-
149
213
  if app is not None and agent is not None:
150
214
  raise TypeError("Only one of 'agent' or 'app' can be provided.")
151
215
 
@@ -154,6 +218,8 @@ class AgentkitAgentServerApp(BaseAgentkitApp):
154
218
  raise TypeError("Either 'agent' or 'app' must be provided.")
155
219
 
156
220
  root_agent = entry.root_agent if isinstance(entry, App) else entry
221
+ session_service = _resolve_session_service(short_term_memory)
222
+ memory_service = _resolve_memory_service(root_agent)
157
223
 
158
224
  _artifact_service = InMemoryArtifactService()
159
225
  _credential_service = InMemoryCredentialService()
@@ -163,12 +229,8 @@ class AgentkitAgentServerApp(BaseAgentkitApp):
163
229
 
164
230
  self.server = AdkWebServer(
165
231
  agent_loader=AgentKitAgentLoader(entry),
166
- session_service=short_term_memory
167
- if isinstance(short_term_memory, BaseSessionService)
168
- else short_term_memory.session_service,
169
- memory_service=root_agent.long_term_memory
170
- if isinstance(root_agent, Agent) and root_agent.long_term_memory
171
- else InMemoryMemoryService(),
232
+ session_service=session_service,
233
+ memory_service=memory_service,
172
234
  artifact_service=_artifact_service,
173
235
  credential_service=_credential_service,
174
236
  eval_sets_manager=_eval_sets_manager,
@@ -176,11 +238,13 @@ class AgentkitAgentServerApp(BaseAgentkitApp):
176
238
  agents_dir=".",
177
239
  )
178
240
 
179
- runner = Runner(
180
- agent=root_agent,
181
- short_term_memory=short_term_memory
182
- if isinstance(short_term_memory, ShortTermMemory)
183
- else None,
241
+ runner = _create_a2a_runner(
242
+ root_agent=root_agent,
243
+ short_term_memory=short_term_memory,
244
+ session_service=session_service,
245
+ memory_service=memory_service,
246
+ artifact_service=_artifact_service,
247
+ credential_service=_credential_service,
184
248
  )
185
249
  _a2a_server_app = to_a2a(agent=root_agent, runner=runner)
186
250
 
@@ -0,0 +1,5 @@
1
+ from agentkit.apps.langgraph_server_app.langgraph_server_app import (
2
+ AgentkitLangGraphServerApp,
3
+ )
4
+
5
+ __all__ = ["AgentkitLangGraphServerApp"]
@@ -0,0 +1,741 @@
1
+ """AgentKit protocol routes for LangGraph Server applications."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import json
7
+ import logging
8
+ import os
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Any
12
+ from uuid import NAMESPACE_URL, uuid4, uuid5
13
+
14
+ import uvicorn
15
+ from fastapi import Request
16
+ from fastapi.responses import JSONResponse, StreamingResponse
17
+ from pydantic import AliasChoices, BaseModel, ConfigDict, Field
18
+ from starlette.responses import Response
19
+ from starlette.routing import Mount
20
+ from typing_extensions import override
21
+
22
+ from agentkit.apps.base_app import BaseAgentkitApp
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _OUTPUT_FIELD_KEYS = ("answer", "output", "final", "response", "text", "messages")
27
+ _THREAD_ID_NAMESPACE = "agentkit:langgraph-server"
28
+
29
+
30
+ class AgentkitRunRequest(BaseModel):
31
+ """Minimal AgentKit run request model for LangGraph Server mode."""
32
+
33
+ model_config = ConfigDict(populate_by_name=True)
34
+
35
+ app_name: str = Field(validation_alias=AliasChoices("appName", "app_name"))
36
+ user_id: str = Field(validation_alias=AliasChoices("userId", "user_id"))
37
+ session_id: str = Field(validation_alias=AliasChoices("sessionId", "session_id"))
38
+ new_message: Any | None = Field(
39
+ default=None,
40
+ validation_alias=AliasChoices("newMessage", "new_message"),
41
+ )
42
+ streaming: bool | None = None
43
+ state_delta: dict[str, Any] | None = Field(
44
+ default=None,
45
+ validation_alias=AliasChoices("stateDelta", "state_delta"),
46
+ )
47
+
48
+
49
+ def _load_json(path: Path) -> dict[str, Any]:
50
+ try:
51
+ data = json.loads(path.read_text(encoding="utf-8"))
52
+ except FileNotFoundError as exc:
53
+ raise FileNotFoundError(f"LangGraph config file not found: {path}") from exc
54
+ except json.JSONDecodeError as exc:
55
+ raise ValueError(f"LangGraph config file is not valid JSON: {path}") from exc
56
+ if not isinstance(data, dict):
57
+ raise ValueError("LangGraph config must be a JSON object.")
58
+ return data
59
+
60
+
61
+ def _load_dotenv(path: Path) -> dict[str, str]:
62
+ if not path.exists():
63
+ return {}
64
+ try:
65
+ from dotenv.main import DotEnv
66
+ except ImportError:
67
+ logger.warning("python-dotenv is not installed; skipping %s.", path)
68
+ return {}
69
+ values = DotEnv(dotenv_path=path).dict() or {}
70
+ return {key: value for key, value in values.items() if value is not None}
71
+
72
+
73
+ def _set_env_json(name: str, value: Any) -> None:
74
+ if value is None:
75
+ os.environ.pop(name, None)
76
+ else:
77
+ os.environ[name] = json.dumps(value)
78
+
79
+
80
+ def _json_text(value: Any) -> str:
81
+ try:
82
+ return json.dumps(value, ensure_ascii=False, default=str)
83
+ except Exception:
84
+ return str(value)
85
+
86
+
87
+ def _content_value_to_text(content: Any) -> str:
88
+ if content is None:
89
+ return ""
90
+ if isinstance(content, str):
91
+ return content
92
+ if isinstance(content, dict):
93
+ for key in ("text", "content", "output", "answer"):
94
+ if key in content:
95
+ text = _content_value_to_text(content[key])
96
+ if text:
97
+ return text
98
+ return _json_text(content)
99
+ if isinstance(content, list):
100
+ return "".join(_content_value_to_text(item) for item in content)
101
+ text = getattr(content, "text", None)
102
+ if isinstance(text, str):
103
+ return text
104
+ return str(content)
105
+
106
+
107
+ def _chunk_to_text(value: Any) -> str:
108
+ if value is None:
109
+ return ""
110
+ if isinstance(value, str):
111
+ return value
112
+ content = getattr(value, "content", None)
113
+ if content is not None:
114
+ return _content_value_to_text(content)
115
+ text = getattr(value, "text", None)
116
+ if isinstance(text, str):
117
+ return text
118
+ if isinstance(value, dict):
119
+ for key in ("output", "answer", "text", "content"):
120
+ if key in value:
121
+ text = _chunk_to_text(value[key])
122
+ if text:
123
+ return text
124
+ messages = value.get("messages")
125
+ if isinstance(messages, (list, tuple)) and messages:
126
+ return _chunk_to_text(messages[-1])
127
+ for nested in value.values():
128
+ text = _chunk_to_text(nested)
129
+ if text:
130
+ return text
131
+ return _json_text(value)
132
+ if isinstance(value, (list, tuple)):
133
+ for item in reversed(value):
134
+ text = _chunk_to_text(item)
135
+ if text:
136
+ return text
137
+ return ""
138
+ return str(value)
139
+
140
+
141
+ def _chunk_delta(accumulated: str, text: str) -> str:
142
+ if not accumulated:
143
+ return text
144
+ if text.startswith(accumulated):
145
+ return text[len(accumulated) :]
146
+ return text
147
+
148
+
149
+ def _normalize_import_path(config_dir: Path, value: Any) -> Any:
150
+ if not isinstance(value, str) or ":" not in value:
151
+ return value
152
+ path_or_module, variable = value.rsplit(":", 1)
153
+ if not path_or_module or os.path.isabs(path_or_module):
154
+ return value
155
+ if "/" not in path_or_module and not path_or_module.endswith(".py"):
156
+ return value
157
+ return f"{(config_dir / path_or_module).resolve()}:{variable}"
158
+
159
+
160
+ def _normalize_path_config(config_dir: Path, value: Any) -> Any:
161
+ if not isinstance(value, dict):
162
+ return value
163
+ normalized = dict(value)
164
+ if "path" in normalized:
165
+ normalized["path"] = _normalize_import_path(config_dir, normalized["path"])
166
+ if "app" in normalized:
167
+ normalized["app"] = _normalize_import_path(config_dir, normalized["app"])
168
+ return normalized
169
+
170
+
171
+ def _normalize_config_paths(config_dir: Path, config: dict[str, Any]) -> dict[str, Any]:
172
+ normalized = dict(config)
173
+ graphs = config.get("graphs")
174
+ if isinstance(graphs, dict):
175
+ normalized_graphs: dict[str, Any] = {}
176
+ for graph_id, graph_config in graphs.items():
177
+ if isinstance(graph_config, str):
178
+ normalized_graphs[graph_id] = _normalize_import_path(config_dir, graph_config)
179
+ elif isinstance(graph_config, dict):
180
+ normalized_graphs[graph_id] = _normalize_path_config(config_dir, graph_config)
181
+ else:
182
+ normalized_graphs[graph_id] = graph_config
183
+ normalized["graphs"] = normalized_graphs
184
+ for key in ("auth", "checkpointer", "http"):
185
+ normalized[key] = _normalize_path_config(config_dir, config.get(key))
186
+ return normalized
187
+
188
+
189
+ def _graph_source(value: Any) -> str | None:
190
+ if isinstance(value, str):
191
+ return value
192
+ if isinstance(value, dict) and isinstance(value.get("path"), str):
193
+ return value["path"]
194
+ return None
195
+
196
+
197
+ def _materialize_lazy_graph_exports(graphs: Any) -> None:
198
+ if not isinstance(graphs, dict):
199
+ return
200
+ for graph_config in graphs.values():
201
+ source = _graph_source(graph_config)
202
+ if not source or ":" not in source:
203
+ continue
204
+ module_name, variable = source.rsplit(":", 1)
205
+ if "/" in module_name or module_name.endswith(".py"):
206
+ continue
207
+ try:
208
+ module = importlib.import_module(module_name)
209
+ except Exception:
210
+ # This helper only makes lazy module exports visible to LangGraph's
211
+ # loader. Import errors still belong to the official loader path.
212
+ continue
213
+ if variable in module.__dict__:
214
+ continue
215
+ try:
216
+ module.__dict__[variable] = getattr(module, variable)
217
+ except Exception:
218
+ continue
219
+
220
+
221
+ def _extend_python_path(config_dir: Path, config: dict[str, Any]) -> None:
222
+ candidates = [config_dir]
223
+ dependencies = config.get("dependencies")
224
+ if isinstance(dependencies, list):
225
+ for dependency in dependencies:
226
+ if not isinstance(dependency, str):
227
+ continue
228
+ dependency_path = (config_dir / dependency).resolve()
229
+ if dependency_path.is_dir():
230
+ candidates.append(dependency_path)
231
+ for candidate in reversed(candidates):
232
+ value = str(candidate)
233
+ if value not in sys.path:
234
+ sys.path.insert(0, value)
235
+
236
+
237
+ def _prepare_langgraph_environment(
238
+ config_path: Path,
239
+ config: dict[str, Any],
240
+ *,
241
+ allow_blocking: bool | None = None,
242
+ ) -> None:
243
+ config_dir = config_path.parent
244
+ env_config = config.get("env")
245
+ if isinstance(env_config, str):
246
+ env_path = (config_dir / env_config).resolve()
247
+ for key, value in _load_dotenv(env_path).items():
248
+ os.environ.setdefault(key, value)
249
+ elif isinstance(env_config, dict):
250
+ for key, value in env_config.items():
251
+ if isinstance(key, str) and value is not None:
252
+ os.environ.setdefault(key, str(value))
253
+
254
+ os.environ["LANGSERVE_GRAPHS"] = json.dumps(config.get("graphs", {}))
255
+ _set_env_json("LANGGRAPH_AUTH", config.get("auth"))
256
+ _set_env_json("LANGGRAPH_HTTP", config.get("http"))
257
+ _set_env_json("LANGGRAPH_STORE", config.get("store"))
258
+ _set_env_json("LANGGRAPH_UI", config.get("ui"))
259
+ _set_env_json("LANGGRAPH_WEBHOOKS", config.get("webhooks"))
260
+ _set_env_json("LANGGRAPH_UI_CONFIG", config.get("ui_config"))
261
+ _set_env_json("LANGGRAPH_CHECKPOINTER", config.get("checkpointer"))
262
+ os.environ.setdefault("DATABASE_URI", ":memory:")
263
+ os.environ.setdefault("REDIS_URI", "fake")
264
+ os.environ.setdefault("MIGRATIONS_PATH", "__inmem")
265
+ os.environ.setdefault("N_JOBS_PER_WORKER", "1")
266
+ os.environ.setdefault("LANGSMITH_LANGGRAPH_API_VARIANT", "local_dev")
267
+ os.environ.setdefault("LANGGRAPH_RUNTIME_EDITION", "inmem")
268
+ os.environ.setdefault("LANGGRAPH_API_URL", os.environ.get("AGENTKIT_BASE_URL", "http://localhost:8000"))
269
+ os.environ.setdefault("LANGGRAPH_DISABLE_FILE_PERSISTENCE", "false")
270
+ if allow_blocking is None:
271
+ os.environ.setdefault("LANGGRAPH_ALLOW_BLOCKING", "false")
272
+ else:
273
+ os.environ["LANGGRAPH_ALLOW_BLOCKING"] = str(allow_blocking).lower()
274
+ os.environ.setdefault("ALLOW_PRIVATE_NETWORK", "true")
275
+
276
+
277
+ def _resolve_graph_id(config: dict[str, Any], graph_id: str | None) -> str:
278
+ graphs = config.get("graphs")
279
+ if not isinstance(graphs, dict) or not graphs:
280
+ raise ValueError("LangGraph config must define at least one graph.")
281
+ if graph_id:
282
+ if graph_id not in graphs:
283
+ raise ValueError(f"Graph id {graph_id!r} was not found in langgraph.json.")
284
+ return graph_id
285
+ if len(graphs) > 1:
286
+ choices = ", ".join(str(key) for key in graphs)
287
+ raise ValueError(f"Multiple LangGraph graphs found; specify graph_id. Available: {choices}.")
288
+ return next(iter(graphs))
289
+
290
+
291
+ def _content_text(content: Any | None) -> str:
292
+ if content is None:
293
+ return ""
294
+ if isinstance(content, str):
295
+ return content
296
+ if isinstance(content, dict):
297
+ parts = content.get("parts") or []
298
+ if not parts and isinstance(content.get("text"), str):
299
+ return content["text"]
300
+ if not parts and isinstance(content.get("content"), str):
301
+ return content["content"]
302
+ else:
303
+ parts = getattr(content, "parts", None) or []
304
+ texts: list[str] = []
305
+ for part in parts:
306
+ text = part.get("text") if isinstance(part, dict) else getattr(part, "text", None)
307
+ if text:
308
+ texts.append(str(text))
309
+ return "\n".join(texts)
310
+
311
+
312
+ def _input_payload(text: str, input_key: str | None) -> dict[str, Any]:
313
+ if input_key:
314
+ return {input_key: text}
315
+ return {"messages": [{"role": "user", "content": text}]}
316
+
317
+
318
+ def _update_output_text(data: Any) -> str:
319
+ if not isinstance(data, dict):
320
+ return _chunk_to_text(data)
321
+ for key in _OUTPUT_FIELD_KEYS:
322
+ if key in data:
323
+ text = _stream_message_text(data[key]) if key == "messages" else _chunk_to_text(data[key])
324
+ if text:
325
+ return text
326
+ latest = ""
327
+ for value in data.values():
328
+ if not isinstance(value, dict):
329
+ continue
330
+ for key in _OUTPUT_FIELD_KEYS:
331
+ if key in value:
332
+ text = _stream_message_text(value[key]) if key == "messages" else _chunk_to_text(value[key])
333
+ if text:
334
+ latest = text
335
+ return latest
336
+
337
+
338
+ def _stream_chunk(chunk: Any) -> tuple[str, Any]:
339
+ if isinstance(chunk, dict):
340
+ return str(chunk.get("type") or chunk.get("event") or ""), chunk.get("data")
341
+ event = getattr(chunk, "event", "")
342
+ data = getattr(chunk, "data", None)
343
+ return str(event or ""), data
344
+
345
+
346
+ def _jsonable(value: Any) -> Any:
347
+ try:
348
+ json.dumps(value, ensure_ascii=False)
349
+ return value
350
+ except Exception:
351
+ return str(value)
352
+
353
+
354
+ def _interrupt_payload(data: Any) -> Any | None:
355
+ if not isinstance(data, dict):
356
+ return None
357
+ if "__interrupt__" in data:
358
+ interrupts = data.get("__interrupt__")
359
+ if not isinstance(interrupts, (list, tuple)) or not interrupts:
360
+ return _jsonable(interrupts)
361
+ values: list[dict[str, Any]] = []
362
+ for interrupt in interrupts:
363
+ values.append(
364
+ {
365
+ "id": getattr(interrupt, "id", None),
366
+ "value": _jsonable(getattr(interrupt, "value", interrupt)),
367
+ }
368
+ )
369
+ return values
370
+ for value in data.values():
371
+ interrupt = _interrupt_payload(value)
372
+ if interrupt is not None:
373
+ return interrupt
374
+ return None
375
+
376
+
377
+ def _message_type(value: Any) -> str:
378
+ if isinstance(value, dict):
379
+ raw = value.get("type") or value.get("role") or ""
380
+ else:
381
+ raw = getattr(value, "type", None) or getattr(value, "role", None) or value.__class__.__name__
382
+ return str(raw).lower()
383
+
384
+
385
+ def _is_ai_message(value: Any) -> bool:
386
+ message_type = _message_type(value)
387
+ return message_type in {"ai", "assistant", "model", "aimessage", "aimessagechunk"}
388
+
389
+
390
+ def _is_empty_message_content(value: Any) -> bool:
391
+ if isinstance(value, dict):
392
+ content = value.get("content")
393
+ has_text = isinstance(value.get("text"), str) and bool(value.get("text"))
394
+ else:
395
+ content = getattr(value, "content", None)
396
+ has_text = isinstance(getattr(value, "text", None), str) and bool(getattr(value, "text", None))
397
+ return not has_text and (content is None or content == "" or content == {} or content == [])
398
+
399
+
400
+ def _message_candidates(data: Any) -> list[Any]:
401
+ if isinstance(data, dict):
402
+ messages = data.get("messages")
403
+ if isinstance(messages, (list, tuple)):
404
+ return list(messages)
405
+ return [data]
406
+ if isinstance(data, (list, tuple)):
407
+ return list(data)
408
+ return [data]
409
+
410
+
411
+ def _stream_message_text(data: Any) -> str:
412
+ latest = ""
413
+ for candidate in _message_candidates(data):
414
+ if _is_ai_message(candidate) and not _is_empty_message_content(candidate):
415
+ text = _chunk_to_text(candidate)
416
+ if text:
417
+ latest = text
418
+ return latest
419
+
420
+
421
+ def _agentkit_text_event(agent_name: str, text: str, *, partial: bool) -> dict[str, Any]:
422
+ return {
423
+ "invocationId": f"agentkit-langgraph-{uuid4()}",
424
+ "author": agent_name,
425
+ "partial": partial,
426
+ "content": {"role": "model", "parts": [{"text": text}]},
427
+ }
428
+
429
+
430
+ def _interrupt_event(agent_name: str, interrupt: Any) -> dict[str, Any]:
431
+ return {
432
+ "invocationId": f"agentkit-langgraph-{uuid4()}",
433
+ "author": agent_name,
434
+ "interrupted": True,
435
+ "errorCode": "LANGGRAPH_INTERRUPT",
436
+ "errorMessage": "LangGraph execution interrupted and requires resume input.",
437
+ "customMetadata": {"langgraph_interrupt": interrupt},
438
+ }
439
+
440
+
441
+ def _event_json(event: dict[str, Any]) -> str:
442
+ return json.dumps(event, ensure_ascii=False, separators=(",", ":"))
443
+
444
+
445
+ def _agentkit_thread_id(app_name: str, user_id: str, session_id: str) -> str:
446
+ raw = f"{_THREAD_ID_NAMESPACE}:{app_name}:{user_id}:{session_id}"
447
+ return str(uuid5(NAMESPACE_URL, raw))
448
+
449
+
450
+ def _session_body(app_name: str, user_id: str, session_id: str, state: dict[str, Any] | None = None) -> dict[str, Any]:
451
+ return {
452
+ "id": session_id,
453
+ "appName": app_name,
454
+ "userId": user_id,
455
+ "state": state or {},
456
+ "events": [],
457
+ }
458
+
459
+
460
+ async def _json_body_or_empty(request: Request) -> dict[str, Any]:
461
+ body = await request.body()
462
+ if not body:
463
+ return {}
464
+ try:
465
+ payload = json.loads(body.decode("utf-8"))
466
+ except (UnicodeDecodeError, json.JSONDecodeError):
467
+ return {}
468
+ return payload if isinstance(payload, dict) else {}
469
+
470
+
471
+ class AgentkitLangGraphServerApp(BaseAgentkitApp):
472
+ """Run a LangGraph Server app and expose AgentKit-compatible routes.
473
+
474
+ LangGraph remains the execution runtime. AgentKit routes translate requests
475
+ into LangGraph threads/runs so persistence, interrupts, runtime context, and
476
+ graph factories keep their original LangGraph Server semantics.
477
+ """
478
+
479
+ def __init__(
480
+ self,
481
+ *,
482
+ config_path: str | Path = "langgraph.json",
483
+ graph_id: str | None = None,
484
+ input_key: str | None = None,
485
+ allow_blocking: bool | None = None,
486
+ ) -> None:
487
+ super().__init__()
488
+ self.config_path = Path(config_path).resolve()
489
+ raw_config = _load_json(self.config_path)
490
+ self.config = _normalize_config_paths(self.config_path.parent, raw_config)
491
+ self.graph_id = _resolve_graph_id(self.config, graph_id)
492
+ self.input_key = input_key
493
+ _extend_python_path(self.config_path.parent, self.config)
494
+ _prepare_langgraph_environment(self.config_path, self.config, allow_blocking=allow_blocking)
495
+ _materialize_lazy_graph_exports(self.config.get("graphs"))
496
+ try:
497
+ server_module = importlib.import_module("langgraph_api.server")
498
+ from langgraph_sdk import get_client
499
+ except ImportError as exc:
500
+ raise ImportError(
501
+ "LangGraph server mode requires langgraph-api and langgraph-sdk. "
502
+ "Install langgraph-cli[inmem] or use the requirements generated by agentkit migrate."
503
+ ) from exc
504
+ self.app = server_module.app
505
+ self._client = get_client(url=None, api_key=None)
506
+ self._attach_agentkit_routes()
507
+
508
+ def _run_kwargs(self, req: AgentkitRunRequest) -> dict[str, Any]:
509
+ app_name = req.app_name or self.graph_id
510
+ thread_id = _agentkit_thread_id(app_name, req.user_id, req.session_id)
511
+ text = _content_text(req.new_message)
512
+ configurable = {
513
+ "thread_id": thread_id,
514
+ "user_id": req.user_id,
515
+ "agentkit_session_id": req.session_id,
516
+ "agentkit_thread_id": thread_id,
517
+ }
518
+ if req.state_delta:
519
+ configurable.update(req.state_delta)
520
+ return {
521
+ "thread_id": thread_id,
522
+ "assistant_id": self.graph_id,
523
+ "input": _input_payload(text, self.input_key),
524
+ "config": {"configurable": configurable},
525
+ "metadata": {
526
+ "agentkit_user_id": req.user_id,
527
+ "agentkit_session_id": req.session_id,
528
+ "agentkit_app_name": req.app_name or self.graph_id,
529
+ },
530
+ "if_not_exists": "create",
531
+ }
532
+
533
+ async def _ensure_thread(self, app_name: str, user_id: str, session_id: str, state: dict[str, Any] | None = None) -> None:
534
+ thread_id = _agentkit_thread_id(app_name, user_id, session_id)
535
+ metadata = {
536
+ "agentkit_app_name": app_name,
537
+ "agentkit_user_id": user_id,
538
+ "agentkit_session_id": session_id,
539
+ **(state or {}),
540
+ }
541
+ await self._client.threads.create(
542
+ thread_id=thread_id,
543
+ if_exists="do_nothing",
544
+ graph_id=self.graph_id,
545
+ metadata=metadata,
546
+ )
547
+
548
+ async def _stream_events(self, req: AgentkitRunRequest):
549
+ await self._ensure_thread(req.app_name or self.graph_id, req.user_id, req.session_id)
550
+ accumulated_text = ""
551
+ final_text = ""
552
+ async for chunk in self._client.runs.stream(
553
+ **self._run_kwargs(req),
554
+ stream_mode=["messages", "updates", "values"],
555
+ version="v2",
556
+ ):
557
+ mode, data = _stream_chunk(chunk)
558
+ if mode == "messages" or mode.startswith("messages/"):
559
+ text = _stream_message_text(data)
560
+ delta = _chunk_delta(accumulated_text, text)
561
+ if delta:
562
+ accumulated_text += delta
563
+ yield _agentkit_text_event(self.graph_id, delta, partial=True)
564
+ if text:
565
+ final_text = text
566
+ continue
567
+ if mode in {"updates", "values"}:
568
+ interrupt = _interrupt_payload(data)
569
+ if interrupt is not None:
570
+ yield _interrupt_event(self.graph_id, interrupt)
571
+ return
572
+ text = _update_output_text(data)
573
+ if text:
574
+ final_text = text
575
+ final_text = final_text or accumulated_text
576
+ if final_text:
577
+ yield _agentkit_text_event(self.graph_id, final_text, partial=False)
578
+
579
+ def _attach_agentkit_routes(self) -> None:
580
+ async def health(request: Request) -> JSONResponse:
581
+ del request
582
+ return JSONResponse({"status": "ok"})
583
+
584
+ async def list_apps(request: Request) -> JSONResponse:
585
+ detailed = request.query_params.get("detailed") == "true"
586
+ if detailed:
587
+ return JSONResponse(
588
+ {
589
+ "apps": [
590
+ {
591
+ "name": self.graph_id,
592
+ "root_agent_name": self.graph_id,
593
+ "description": "LangGraph Server app exposed through AgentKit routes",
594
+ "language": "python",
595
+ }
596
+ ]
597
+ }
598
+ )
599
+ return JSONResponse([self.graph_id])
600
+
601
+ async def get_session(request: Request) -> JSONResponse:
602
+ session_id = request.path_params["session_id"]
603
+ app_name = request.path_params["app_name"]
604
+ user_id = request.path_params["user_id"]
605
+ thread_id = _agentkit_thread_id(app_name, user_id, session_id)
606
+ response = await self._client.threads.get(thread_id)
607
+ metadata = response.get("metadata", {}) if isinstance(response, dict) else {}
608
+ return JSONResponse(_session_body(app_name, user_id, session_id, metadata))
609
+
610
+ async def list_sessions(request: Request) -> JSONResponse:
611
+ app_name = request.path_params["app_name"]
612
+ user_id = request.path_params["user_id"]
613
+ response = await self._client.threads.search(
614
+ metadata={"agentkit_app_name": app_name, "agentkit_user_id": user_id},
615
+ limit=100,
616
+ )
617
+ sessions = [
618
+ _session_body(
619
+ app_name,
620
+ user_id,
621
+ item.get("metadata", {}).get("agentkit_session_id", item.get("thread_id", "")),
622
+ item.get("metadata", {}),
623
+ )
624
+ for item in response
625
+ if isinstance(item, dict) and item.get("thread_id")
626
+ ]
627
+ return JSONResponse(sessions)
628
+
629
+ async def create_session(request: Request) -> JSONResponse:
630
+ app_name = request.path_params["app_name"]
631
+ user_id = request.path_params["user_id"]
632
+ payload = await _json_body_or_empty(request)
633
+ session_id = str(payload.get("sessionId") or payload.get("session_id") or uuid4())
634
+ state = payload.get("state") if isinstance(payload.get("state"), dict) else {}
635
+ await self._ensure_thread(app_name, user_id, session_id, state)
636
+ return JSONResponse(_session_body(app_name, user_id, session_id, state))
637
+
638
+ async def create_session_with_id(request: Request) -> JSONResponse:
639
+ app_name = request.path_params["app_name"]
640
+ user_id = request.path_params["user_id"]
641
+ session_id = request.path_params["session_id"]
642
+ payload = await _json_body_or_empty(request)
643
+ state = payload
644
+ await self._ensure_thread(app_name, user_id, session_id, state)
645
+ return JSONResponse(_session_body(app_name, user_id, session_id, state))
646
+
647
+ async def delete_session(request: Request) -> Response:
648
+ thread_id = _agentkit_thread_id(
649
+ request.path_params["app_name"],
650
+ request.path_params["user_id"],
651
+ request.path_params["session_id"],
652
+ )
653
+ await self._client.threads.delete(thread_id)
654
+ return Response(status_code=204)
655
+
656
+ async def update_session(request: Request) -> JSONResponse:
657
+ app_name = request.path_params["app_name"]
658
+ user_id = request.path_params["user_id"]
659
+ session_id = request.path_params["session_id"]
660
+ payload = await _json_body_or_empty(request)
661
+ state_delta = payload.get("stateDelta") or payload.get("state_delta")
662
+ state = state_delta if isinstance(state_delta, dict) else {}
663
+ thread_id = _agentkit_thread_id(app_name, user_id, session_id)
664
+ await self._client.threads.update(thread_id, metadata=state)
665
+ return JSONResponse(_session_body(app_name, user_id, session_id, state))
666
+
667
+ async def run_agent(request: Request) -> JSONResponse:
668
+ req = AgentkitRunRequest.model_validate(await request.json())
669
+ events = [event async for event in self._stream_events(req)]
670
+ return JSONResponse(events)
671
+
672
+ def stream_response(req: AgentkitRunRequest) -> StreamingResponse:
673
+ async def event_generator():
674
+ try:
675
+ async for event in self._stream_events(req):
676
+ yield f"data: {_event_json(event)}\n\n"
677
+ except Exception as exc:
678
+ logger.exception("Error in LangGraph Server AgentKit stream: %s", exc)
679
+ yield f"data: {json.dumps({'error': str(exc)}, ensure_ascii=False)}\n\n"
680
+
681
+ return StreamingResponse(event_generator(), media_type="text/event-stream")
682
+
683
+ async def run_agent_sse(request: Request) -> StreamingResponse:
684
+ req = AgentkitRunRequest.model_validate(await request.json())
685
+ return stream_response(req)
686
+
687
+ async def invoke_compat(request: Request) -> StreamingResponse:
688
+ try:
689
+ payload = await request.json()
690
+ except Exception:
691
+ payload = {}
692
+ prompt = payload.get("prompt") if isinstance(payload, dict) else None
693
+ if prompt is None:
694
+ prompt = json.dumps(payload, ensure_ascii=False) if payload else ""
695
+ user_id = request.headers.get("user_id") or "agentkit_user"
696
+ session_id = request.headers.get("session_id") or str(uuid4())
697
+ req = AgentkitRunRequest(
698
+ app_name=self.graph_id,
699
+ user_id=user_id,
700
+ session_id=session_id,
701
+ new_message={"role": "user", "parts": [{"text": str(prompt)}]},
702
+ streaming=True,
703
+ )
704
+ return stream_response(req)
705
+
706
+ routes = [
707
+ ("/health", health, ["GET"]),
708
+ ("/list-apps", list_apps, ["GET"]),
709
+ ("/apps/{app_name}/users/{user_id}/sessions/{session_id}", get_session, ["GET"]),
710
+ ("/apps/{app_name}/users/{user_id}/sessions", list_sessions, ["GET"]),
711
+ ("/apps/{app_name}/users/{user_id}/sessions", create_session, ["POST"]),
712
+ ("/apps/{app_name}/users/{user_id}/sessions/{session_id}", create_session_with_id, ["POST"]),
713
+ ("/apps/{app_name}/users/{user_id}/sessions/{session_id}", delete_session, ["DELETE"]),
714
+ ("/apps/{app_name}/users/{user_id}/sessions/{session_id}", update_session, ["PATCH"]),
715
+ ("/run", run_agent, ["POST"]),
716
+ ("/run_sse", run_agent_sse, ["POST"]),
717
+ ("/invoke", invoke_compat, ["POST"]),
718
+ ]
719
+ existing = {
720
+ (getattr(route, "path", ""), tuple(sorted(getattr(route, "methods", ()) or ())))
721
+ for route in getattr(self.app, "routes", [])
722
+ }
723
+ for path, endpoint, methods in routes:
724
+ key = (path, tuple(sorted(methods)))
725
+ if key not in existing:
726
+ self.app.add_route(path, endpoint, methods=methods)
727
+ route = self.app.routes.pop()
728
+ insert_at = next(
729
+ (
730
+ index
731
+ for index, existing_route in enumerate(self.app.routes)
732
+ if isinstance(existing_route, Mount)
733
+ and getattr(existing_route, "path", None) == ""
734
+ ),
735
+ len(self.app.routes),
736
+ )
737
+ self.app.routes.insert(insert_at, route)
738
+
739
+ @override
740
+ def run(self, host: str | None = None, port: int = 8000) -> None:
741
+ uvicorn.run(self.app, host=host or "0.0.0.0", port=port)
agentkit/auth/admin.py CHANGED
@@ -344,6 +344,7 @@ def sso_setup(
344
344
  bucket = bucket or f"agentkit-cli-{acct}"
345
345
  url = publish_discovery(
346
346
  coords, bucket=bucket, custom_domain=custom_domain, access_key=api.ak, secret_key=api.sk,
347
+ session_token=api.token,
347
348
  )
348
349
  if custom_domain:
349
350
  manual.append(
@@ -360,6 +361,7 @@ def publish_discovery(
360
361
  custom_domain: str | None = None,
361
362
  access_key: str | None = None,
362
363
  secret_key: str | None = None,
364
+ session_token: str | None = None,
363
365
  ) -> str:
364
366
  """Publish the discovery doc to TOS static hosting (real https). Returns the login URL.
365
367
 
@@ -379,8 +381,9 @@ def publish_discovery(
379
381
  ) from exc
380
382
  ak = access_key or _os.getenv("VOLCENGINE_ACCESS_KEY")
381
383
  sk = secret_key or _os.getenv("VOLCENGINE_SECRET_KEY")
384
+ token = session_token or _os.getenv("VOLCENGINE_SESSION_TOKEN")
382
385
  endpoint = f"tos-{coords.region}.volces.com"
383
- client = tos.TosClientV2(ak, sk, endpoint, coords.region)
386
+ client = tos.TosClientV2(ak, sk, endpoint, coords.region, security_token=token)
384
387
  try:
385
388
  client.create_bucket(bucket, acl=tos.ACLType.ACL_Public_Read)
386
389
  except Exception as exc: # noqa: BLE001
@@ -6,6 +6,7 @@ from collections.abc import AsyncGenerator, Iterable
6
6
  import hashlib
7
7
  import inspect
8
8
  import json
9
+ from dataclasses import dataclass
9
10
  from typing import Any
10
11
 
11
12
  try:
@@ -40,6 +41,12 @@ LANGGRAPH_PENDING_INTERRUPT_STATE_KEY = "agentkit:langgraph:pending_interrupt"
40
41
  _OUTPUT_FIELD_KEYS = ("answer", "output", "final", "response", "text", "messages")
41
42
 
42
43
 
44
+ @dataclass(frozen=True)
45
+ class _FactoryCall:
46
+ args: tuple[Any, ...]
47
+ kwargs: dict[str, Any]
48
+
49
+
43
50
  def _method_kwargs(method: Any, kwargs: dict[str, Any]) -> dict[str, Any]:
44
51
  try:
45
52
  signature = inspect.signature(method)
@@ -51,6 +58,66 @@ def _method_kwargs(method: Any, kwargs: dict[str, Any]) -> dict[str, Any]:
51
58
  return {key: value for key, value in kwargs.items() if accepts_kwargs or key in parameters}
52
59
 
53
60
 
61
+ def _factory_call(factory: Any, config: dict[str, Any] | None) -> _FactoryCall:
62
+ if not callable(factory):
63
+ raise UnsupportedFrameworkAgentError(
64
+ "LangGraph graph_factory=True requires a callable entry that "
65
+ "returns a compiled graph-like object."
66
+ )
67
+
68
+ try:
69
+ signature = inspect.signature(factory)
70
+ except (TypeError, ValueError):
71
+ return _FactoryCall(args=(config,), kwargs={})
72
+
73
+ parameters = list(signature.parameters.values())
74
+ required = [
75
+ param
76
+ for param in parameters
77
+ if param.default is inspect.Parameter.empty
78
+ and param.kind
79
+ in {
80
+ inspect.Parameter.POSITIONAL_ONLY,
81
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
82
+ inspect.Parameter.KEYWORD_ONLY,
83
+ }
84
+ ]
85
+ if len(required) > 1:
86
+ names = ", ".join(param.name for param in required)
87
+ raise UnsupportedFrameworkAgentError(
88
+ "LangGraph Server graph factory entries may require at most one "
89
+ f"RunnableConfig argument; required parameters found: {names}."
90
+ )
91
+
92
+ if any(param.kind == inspect.Parameter.VAR_POSITIONAL for param in parameters):
93
+ return _FactoryCall(args=(config,), kwargs={})
94
+
95
+ if required:
96
+ param = required[0]
97
+ if param.kind == inspect.Parameter.KEYWORD_ONLY:
98
+ return _FactoryCall(args=(), kwargs={param.name: config})
99
+ return _FactoryCall(args=(config,), kwargs={})
100
+
101
+ optional_config = next(
102
+ (
103
+ param
104
+ for param in parameters
105
+ if param.name in {"config", "runnable_config"}
106
+ and param.kind
107
+ in {
108
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
109
+ inspect.Parameter.KEYWORD_ONLY,
110
+ }
111
+ ),
112
+ None,
113
+ )
114
+ if optional_config is None:
115
+ return _FactoryCall(args=(), kwargs={})
116
+ if optional_config.kind == inspect.Parameter.KEYWORD_ONLY:
117
+ return _FactoryCall(args=(), kwargs={optional_config.name: config})
118
+ return _FactoryCall(args=(config,), kwargs={})
119
+
120
+
54
121
  def _jsonable(value: Any) -> Any:
55
122
  try:
56
123
  json.dumps(value, ensure_ascii=False)
@@ -119,6 +186,7 @@ class LangGraphAgentkitBridge(BaseAgent):
119
186
  """Adapt a compiled LangGraph graph to AgentKit's ADK runtime boundary."""
120
187
 
121
188
  _graph: Any = PrivateAttr()
189
+ _graph_factory: bool = PrivateAttr(default=False)
122
190
  _input_key: str | None = PrivateAttr(default=None)
123
191
  _stream_nodes: frozenset[str] = PrivateAttr(default_factory=frozenset)
124
192
 
@@ -130,9 +198,11 @@ class LangGraphAgentkitBridge(BaseAgent):
130
198
  description: str = "LangGraph agent adapted for AgentKit runtime",
131
199
  input_key: str | None = None,
132
200
  stream_nodes: Iterable[str] | str | None = None,
201
+ graph_factory: bool = False,
133
202
  ) -> None:
134
203
  super().__init__(name=name, description=description)
135
204
  self._graph = graph
205
+ self._graph_factory = graph_factory
136
206
  self._input_key = input_key
137
207
  if isinstance(stream_nodes, str):
138
208
  nodes = (stream_nodes,)
@@ -160,6 +230,15 @@ class LangGraphAgentkitBridge(BaseAgent):
160
230
  thread_id = session_id
161
231
  return {"configurable": {"thread_id": thread_id}}
162
232
 
233
+ async def _graph_for_run(self, config: dict[str, Any] | None) -> Any:
234
+ if not self._graph_factory:
235
+ return self._graph
236
+ call = _factory_call(self._graph, config)
237
+ graph = self._graph(*call.args, **call.kwargs)
238
+ if inspect.isawaitable(graph):
239
+ graph = await graph
240
+ return graph
241
+
163
242
  def _pending_interrupt(self, ctx: InvocationContext) -> Any | None:
164
243
  session = getattr(ctx, "session", None)
165
244
  state = getattr(session, "state", None)
@@ -217,12 +296,12 @@ class LangGraphAgentkitBridge(BaseAgent):
217
296
  actions=EventActions(state_delta={LANGGRAPH_PENDING_INTERRUPT_STATE_KEY: None}),
218
297
  )
219
298
 
220
- async def _call_once(self, payload: Any, config: dict[str, Any] | None) -> Any:
221
- ainvoke = getattr(self._graph, "ainvoke", None)
299
+ async def _call_once(self, graph: Any, payload: Any, config: dict[str, Any] | None) -> Any:
300
+ ainvoke = getattr(graph, "ainvoke", None)
222
301
  if callable(ainvoke):
223
302
  return await ainvoke(payload, **_method_kwargs(ainvoke, {"config": config}))
224
303
 
225
- invoke = getattr(self._graph, "invoke", None)
304
+ invoke = getattr(graph, "invoke", None)
226
305
  if callable(invoke):
227
306
  return invoke(payload, **_method_kwargs(invoke, {"config": config}))
228
307
 
@@ -236,8 +315,8 @@ class LangGraphAgentkitBridge(BaseAgent):
236
315
  return ["messages", "updates"]
237
316
  return "updates"
238
317
 
239
- def _sync_stream(self, payload: Any, config: dict[str, Any] | None):
240
- stream_fn = getattr(self._graph, "stream", None)
318
+ def _sync_stream(self, graph: Any, payload: Any, config: dict[str, Any] | None):
319
+ stream_fn = getattr(graph, "stream", None)
241
320
  if not callable(stream_fn):
242
321
  return None
243
322
  stream_mode = self._stream_mode()
@@ -273,15 +352,15 @@ class LangGraphAgentkitBridge(BaseAgent):
273
352
 
274
353
  return iterate_attempts()
275
354
 
276
- async def _stream_graph(self, payload: Any, config: dict[str, Any] | None, *, prefer_sync: bool = False):
355
+ async def _stream_graph(self, graph: Any, payload: Any, config: dict[str, Any] | None, *, prefer_sync: bool = False):
277
356
  if prefer_sync:
278
- stream = self._sync_stream(payload, config)
357
+ stream = self._sync_stream(graph, payload, config)
279
358
  if stream is not None:
280
359
  for item in stream:
281
360
  yield item
282
361
  return
283
362
 
284
- astream = getattr(self._graph, "astream", None)
363
+ astream = getattr(graph, "astream", None)
285
364
  if callable(astream):
286
365
  stream_mode = self._stream_mode()
287
366
  attempts = [
@@ -312,7 +391,7 @@ class LangGraphAgentkitBridge(BaseAgent):
312
391
  if last_error is not None:
313
392
  raise last_error
314
393
 
315
- stream = self._sync_stream(payload, config)
394
+ stream = self._sync_stream(graph, payload, config)
316
395
  if stream is not None:
317
396
  for item in stream:
318
397
  yield item
@@ -337,12 +416,13 @@ class LangGraphAgentkitBridge(BaseAgent):
337
416
  config = self._graph_config(ctx)
338
417
  resume_pending = self._pending_interrupt(ctx) is not None
339
418
  payload = self._run_payload(ctx)
419
+ graph = await self._graph_for_run(config)
340
420
  accumulated_text = ""
341
421
  last_update: Any = None
342
422
  pending_update_text = ""
343
423
  streamed = False
344
424
 
345
- async for item in self._stream_graph(payload, config, prefer_sync=resume_pending):
425
+ async for item in self._stream_graph(graph, payload, config, prefer_sync=resume_pending):
346
426
  streamed = True
347
427
  mode = ""
348
428
  data = item
@@ -400,7 +480,7 @@ class LangGraphAgentkitBridge(BaseAgent):
400
480
  update_text = pending_update_text or _update_output_text(last_update)
401
481
  final_text = update_text or accumulated_text
402
482
  else:
403
- final_text = _update_output_text(await self._call_once(payload, config))
483
+ final_text = _update_output_text(await self._call_once(graph, payload, config))
404
484
 
405
485
  if final_text:
406
486
  yield self._final_event(ctx, final_text, clear_pending_interrupt=resume_pending)
@@ -92,6 +92,9 @@ def _call_zero_arg_factory(target: Any, object_path: str) -> Any:
92
92
 
93
93
  result = target()
94
94
  if inspect.isawaitable(result):
95
+ close = getattr(result, "close", None)
96
+ if callable(close):
97
+ close()
95
98
  raise TypeError(
96
99
  f"Entry factory {object_path!r} returned an awaitable object. "
97
100
  "Async factories are not supported by generated migration apps; "
@@ -330,6 +330,8 @@ def _result_to_text(result: Any) -> str:
330
330
  message_text = _message_to_text(getattr(result, "message", None))
331
331
  if message_text:
332
332
  return message_text
333
+ if hasattr(result, "message") or hasattr(result, "structured_output"):
334
+ return ""
333
335
  if isinstance(result, Exception):
334
336
  return str(result)
335
337
  if isinstance(getattr(result, "results", None), dict):
@@ -83,8 +83,12 @@ MODEL_PROVIDER_CONFIGS: dict[str, ModelProviderConfig] = {
83
83
  ModelProviderType.MODEL_SQUARE.value: ModelProviderConfig(
84
84
  model_base_url="https://ark.cn-beijing.volces.com/api/v3",
85
85
  anthropic_base_url="https://ark.cn-beijing.volces.com/api/compatible",
86
- default_model_name="deepseek-v4-flash-260425",
86
+ default_model_name="glm-5-2-260617",
87
87
  models={
88
+ "glm-5-2-260617": ModelSpec(
89
+ supports_reasoning_summaries=True,
90
+ context_window=DEFAULT_MODEL_CONTEXT_WINDOW,
91
+ ),
88
92
  "doubao-seed-2-0-pro-260215": ModelSpec(
89
93
  supports_reasoning_summaries=True,
90
94
  context_window=LIMITED_MODEL_CONTEXT_WINDOW,
@@ -435,7 +439,11 @@ def build_codex_config_toml(
435
439
  'wire_api = "responses"',
436
440
  # OAuth (ChatGPT login) providers carry no API key - codex uses the injected
437
441
  # auth.json via `requires_openai_auth`; all others use the CODEX_API_KEY env.
438
- ("requires_openai_auth = true" if requires_openai_auth else 'env_key = "CODEX_API_KEY"'),
442
+ (
443
+ "requires_openai_auth = true"
444
+ if requires_openai_auth
445
+ else 'env_key = "CODEX_API_KEY"'
446
+ ),
439
447
  "",
440
448
  "[tui]",
441
449
  "show_tooltips = false",
@@ -552,7 +560,7 @@ if [ -n "$REQ_CODEX_BASE_URL" ]; then
552
560
  CODEX_BASE_URL="$REQ_CODEX_BASE_URL"
553
561
  fi
554
562
 
555
- CODEX_MODEL="${CODEX_MODEL:-deepseek-v4-flash-260425}"
563
+ CODEX_MODEL="${CODEX_MODEL:-glm-5-2-260617}"
556
564
  CODEX_BASE_URL="${CODEX_BASE_URL:-https://ark.cn-beijing.volces.com/api/v3}"
557
565
 
558
566
  : >"$ENV_FILE"
@@ -653,8 +661,10 @@ def _build_model_catalog_item(model_name: str, spec: ModelSpec) -> dict:
653
661
 
654
662
  def infer_model_spec(model_name: str) -> ModelSpec:
655
663
  normalized_model_name = model_name.strip().lower()
656
- if normalized_model_name == "glm-5.2" or normalized_model_name.startswith(
657
- "deepseek-v4"
664
+ if (
665
+ normalized_model_name == "glm-5.2"
666
+ or normalized_model_name.startswith("glm-5-")
667
+ or normalized_model_name.startswith("deepseek-v4")
658
668
  ):
659
669
  context_window = DEFAULT_MODEL_CONTEXT_WINDOW
660
670
  else:
agentkit/version.py CHANGED
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- VERSION = "0.7.10"
15
+ VERSION = "0.7.12"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentkit-sdk-python
3
- Version: 0.7.10
3
+ Version: 0.7.12
4
4
  Summary: Python SDK for transforming any AI agent into a production-ready application. Framework-agnostic primitives for runtime, memory, authentication, and tools with volcengine-managed infrastructure.
5
5
  Author-email: Xiangrui Cheng <innsdcc@gmail.com>, Yumeng Bao <baoyumeng.123@gmail.com>, Yaozheng Fang <fangyozheng@gmail.com>, Guodong Li <cu.eric.lee@gmail.com>
6
6
  License: Apache License
@@ -1,17 +1,19 @@
1
1
  agentkit/__init__.py,sha256=l27ZMDslc3VhmmnPZJyrqVvTDoZ0LqhCtM5hw0caHcU,1021
2
2
  agentkit/errors.py,sha256=UWlXv0ZsXd_oLTMlxuf4u9-XkTdRYeVZueE--Z3JsHU,1214
3
- agentkit/version.py,sha256=elnLJh2dU0M8vTwaqXfngYOIRybHJiYIXsanO6aGgDY,654
4
- agentkit/apps/__init__.py,sha256=-oXTjxV3gejpJ8pffTUcUAXw0LfxKCYZJk-_hIJhtLM,1921
3
+ agentkit/version.py,sha256=UZFajXkO3LMaoMxV_yKH2_pQpvhunh05rfkX_ftKnUQ,654
4
+ agentkit/apps/__init__.py,sha256=_Na1A4NSaMbVfPbsFKj2U-yI5mytD7W5skMaAOGbauY,2328
5
5
  agentkit/apps/base_app.py,sha256=3hZZExL1wyTGWveJEZZoqXN086MzmkVS_WU1vyulIWg,754
6
6
  agentkit/apps/utils.py,sha256=IzimIDmT6FS6PV9MLimPh46mq5zT-EjVmnYPxBdyEq0,1934
7
7
  agentkit/apps/a2a_app/__init__.py,sha256=pkSabKw7_ai4NOo56pXKL40EcaxIDh6HYxPXOY7qWbo,634
8
8
  agentkit/apps/a2a_app/a2a_app.py,sha256=mFuPFuqRlGdQ2VIjQq2_gtQKWwnkwSjSmIMRnammWJU,7605
9
9
  agentkit/apps/a2a_app/telemetry.py,sha256=vR8tf7EHLor62cxs5PBx4mrfHeOaV1a7Xryqlu8nGCU,4290
10
10
  agentkit/apps/agent_server_app/__init__.py,sha256=pkSabKw7_ai4NOo56pXKL40EcaxIDh6HYxPXOY7qWbo,634
11
- agentkit/apps/agent_server_app/agent_server_app.py,sha256=aqLgDGaqV921-BZIe7ZiWMbV9Xjlo7d-cNkDaiXFJo4,17245
11
+ agentkit/apps/agent_server_app/agent_server_app.py,sha256=jq0DR6AZObeBQ1zZO_-qzO6n_ABov9lolL2g0n_ywPE,19535
12
12
  agentkit/apps/agent_server_app/middleware.py,sha256=2gLCRN-JYjYsTQzHqRBU69S2OI8gD5AdbrFeoGzDcM0,2962
13
13
  agentkit/apps/agent_server_app/origin.py,sha256=hzpftk8cW71uFw0KKsBAvrXiIOF8E6Bjo_MS5gDxSYM,5785
14
14
  agentkit/apps/agent_server_app/telemetry.py,sha256=FLJsxJHj4Vg3sfIZnWYm442_Dv-zmxTIPqRgvgwVIFQ,4398
15
+ agentkit/apps/langgraph_server_app/__init__.py,sha256=ta7KORxpAc8cUGFusoTFBZFHuVaQbcsWst8CKBepg1c,146
16
+ agentkit/apps/langgraph_server_app/langgraph_server_app.py,sha256=gEqH6CTeuwE255m06IaTOoc7l5V3_OJF0tiForJqLaQ,29146
15
17
  agentkit/apps/mcp_app/__init__.py,sha256=pkSabKw7_ai4NOo56pXKL40EcaxIDh6HYxPXOY7qWbo,634
16
18
  agentkit/apps/mcp_app/mcp_app.py,sha256=2ngFbKcC3OsWNPwU0aoS-D8JVBC7-mFpXqj7sgcvUY8,5886
17
19
  agentkit/apps/mcp_app/telemetry.py,sha256=ObzEoJrQK9Yu_3KsCslJejIhKkACZl5c87KHMFblvgI,3929
@@ -23,7 +25,7 @@ agentkit/auth/__init__.py,sha256=eIW6ziL3rtBYEupfmqq8MshZ40wL3muYjCQRikFG_j0,364
23
25
  agentkit/auth/_openapi.py,sha256=ofFX7Ch-1xQeOvwUYRU4YwGKUqDgIfI8lsnk8Vbi12s,8230
24
26
  agentkit/auth/_redact.py,sha256=Ef3yLlWj6jw0G6gZKy3cY64X6sm0jhzor8qx8b0Dp4A,911
25
27
  agentkit/auth/_sigv4.py,sha256=yAbFD57g3cBvCkGTVJap0BCakQeukyR_FlzMDYZdTkw,3375
26
- agentkit/auth/admin.py,sha256=U9rvVFx1rD5dsSrL0eQVPezBVI9vhaSrdc6QkM7jVKY,23393
28
+ agentkit/auth/admin.py,sha256=JDyKIptLKMQOAp8DjUvrCRnJ2jER6cZUrXoUJcojMDU,23554
27
29
  agentkit/auth/credential_hosting.py,sha256=lFFYseYHkqCUsv1djtK3EFR1XvESmw6l1Zgu1CcwIa8,24280
28
30
  agentkit/auth/errors.py,sha256=eNNdzXP35QAlYy-jM5I9CQZCSS9rW0cp5_1F0I75JcU,1448
29
31
  agentkit/auth/model_login.py,sha256=_30pbZgs22o4UtGF03dMgFRN9yE4OSyLqXJm9JhEe7E,13775
@@ -44,10 +46,10 @@ agentkit/frameworks/__init__.py,sha256=a9fu6cjLFn8Mq82sIo7rPT1gZGzi_QfCuFWpyoa2I
44
46
  agentkit/frameworks/_common.py,sha256=0L0cyBQxtlOdi3GbQ_UgaOoHWwenztlKrxUsyvV3hXw,3859
45
47
  agentkit/frameworks/agentcore.py,sha256=L5bdqzVukm7V6esIttPZSHFEc8JiqysU8YxA6_pBYRQ,12849
46
48
  agentkit/frameworks/langchain.py,sha256=Tcy2HGYqKmcugDSn3OMaD_6uxhsJtZI3o5E8Rw7UJrg,5947
47
- agentkit/frameworks/langgraph.py,sha256=fc2IbK6PYzSK5a4iPMGa2A9_eMW3Asd0nFlY8JuGLiY,14866
48
- agentkit/frameworks/migration.py,sha256=j8MxFvAFKkDsTy_e1l5bN-B276AE7rwxLrHuBodNcfc,4385
49
+ agentkit/frameworks/langgraph.py,sha256=hSYfh9Gy2vj3UM7zbpl4ZoawypTy6fFbz-Y0YrSS8p0,17651
50
+ agentkit/frameworks/migration.py,sha256=OTIsrqxVOWlrP10D7_2Zt1tGFNWEmmAHqsTuLB7YBhY,4480
49
51
  agentkit/frameworks/model_replacement.py,sha256=EtRl617CZqYLNQ6CYKtK4XzObsv778jQCgWpBaJJahk,5196
50
- agentkit/frameworks/strands.py,sha256=PdetmUHDWg4JK3ZxOmnLo81ZLTlHHRISuTznm51jPRE,20792
52
+ agentkit/frameworks/strands.py,sha256=KFeTsdVpOyOh-SJvC3jcW8FdhR_5Y3NaGDd6zbuYrXE,20885
51
53
  agentkit/frameworks/serving/__init__.py,sha256=EaR0jzP6np8v1gyD6CYlaRoIY1KgBaFosqCmRUgMc2E,302
52
54
  agentkit/frameworks/serving/fastapi_mount.py,sha256=LV6gJunwd7aU8MFdDYSxeaIUYWvYwMl2Am1J07ddP0A,1958
53
55
  agentkit/frameworks/serving/langserve.py,sha256=R2R0g95LHcSFJ1BnOm2cxnULv3wQaZNUdm5Aqd-Oq5s,17238
@@ -137,7 +139,7 @@ agentkit/toolkit/cli/sandbox/cli_shell.py,sha256=f-PJmIClEgsNrY-e8wKhc9th7r7H5_Z
137
139
  agentkit/toolkit/cli/sandbox/cli_web.py,sha256=nvBEp99yqA3-sDNA_PBn7jOG_VgkHTw_8lDSvRNYSRA,2496
138
140
  agentkit/toolkit/cli/sandbox/env_config.py,sha256=mQKKtRG7FXG0vhizEMpkzH9_g2agCQt9FDKgSGf-Xjs,26184
139
141
  agentkit/toolkit/cli/sandbox/git_config.py,sha256=y0omhCCsH33XCMEpmbtAdHM8pevRFQdil1-JfOV8RTo,6097
140
- agentkit/toolkit/cli/sandbox/model_config.py,sha256=j-foFsln5rs67EtLNNzdpPFlro6l_k3IIdpq1YA89j8,23478
142
+ agentkit/toolkit/cli/sandbox/model_config.py,sha256=A3TagXhjYB1HWtsEDIRVUPmVOfc6c_e7qcipSkqCBLk,23752
141
143
  agentkit/toolkit/cli/sandbox/sandbox_client.py,sha256=6HEmHOzXW7LVohGi4NmmeX3d-GQHyz6-C4svwI_Sfr8,15007
142
144
  agentkit/toolkit/cli/sandbox/session_create.py,sha256=LOUqRxhvY4YmaVTnxLQBlLswpq6J6TDtOR6o9yt6Iz0,14947
143
145
  agentkit/toolkit/cli/sandbox/session_sync.py,sha256=yLcRN4dw0dHHYk1kgDvjF8jBv5GnHV8xOLtKtsQTm6s,3152
@@ -251,9 +253,9 @@ agentkit/utils/redact.py,sha256=RAtiggxH74eb1Da6Gg0G5SLTVWRaIE5RqYnCozSZozQ,2167
251
253
  agentkit/utils/request.py,sha256=1IGxPS3BSCKk1K1_j862_v34xzLEx04CIvrLVb1MsGQ,1625
252
254
  agentkit/utils/template_utils.py,sha256=Qjg9V6dEpjAd_yYezIIPwuDsDeeMmp6XTMn5ZECifNc,6336
253
255
  agentkit/utils/ve_sign.py,sha256=IsRKRshTKpOMkNv4bDJoNWUcsBaMGeq-jHCniPMAyo8,13327
254
- agentkit_sdk_python-0.7.10.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
255
- agentkit_sdk_python-0.7.10.dist-info/METADATA,sha256=W66BJetdVKDgdZnYnM0avT84252zSrODeB5Mszn_DWQ,21409
256
- agentkit_sdk_python-0.7.10.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
257
- agentkit_sdk_python-0.7.10.dist-info/entry_points.txt,sha256=fhzZUsvsLXeB4mPaa0SBQiTBqFT404uDAKPaePAOUAE,58
258
- agentkit_sdk_python-0.7.10.dist-info/top_level.txt,sha256=ipy8JF-QQ-V0C1oRFLxsyaW8zwrasfJ-zjAh9vgOc7U,9
259
- agentkit_sdk_python-0.7.10.dist-info/RECORD,,
256
+ agentkit_sdk_python-0.7.12.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
257
+ agentkit_sdk_python-0.7.12.dist-info/METADATA,sha256=K90dgH6c8sePnET9IhGsL-u8BwcOcT0E58EdsKMXG4U,21409
258
+ agentkit_sdk_python-0.7.12.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
259
+ agentkit_sdk_python-0.7.12.dist-info/entry_points.txt,sha256=fhzZUsvsLXeB4mPaa0SBQiTBqFT404uDAKPaePAOUAE,58
260
+ agentkit_sdk_python-0.7.12.dist-info/top_level.txt,sha256=ipy8JF-QQ-V0C1oRFLxsyaW8zwrasfJ-zjAh9vgOc7U,9
261
+ agentkit_sdk_python-0.7.12.dist-info/RECORD,,