Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@
### 修复

- 修复 ADK 恢复能力关闭时被空消息静默转换为新任务、并发 invocation 映射串线、checkpoint ID 重启碰撞、零事件恢复异常和重复审计的问题。
- 改进 ADK Agent 名称不符合标识符规则时的启动诊断,直接提示非法值、入口位置和可用的修复名称,避免只输出 Pydantic 原始校验堆栈。
- 修复 ADK LiteLLM 非法工具参数补丁返回错误响应类型及重复包装,并补齐 MCP 结果补丁的幂等保护。
- 修复 inline TUI 中工具调用完成后被统一移动到回复末尾、历史重绘影响原生滚动、浅色终端输入区域不清晰等问题。
- 修复 Remote Runner 的 chat stream 将文本 delta 放在顶层时无法解析,导致远程回复内容缺失的问题。
- 修复 LangGraph 流式 usage chunk 重复、缺少 run id 或与最终事件并存时可能重复累计或丢失的问题。
- 修复 LangGraph checkpoint 恢复完成后仍读取源 checkpoint 快照,导致最终输出和后续 checkpoint 元数据停留在恢复前状态的问题。
- 修复 Langfuse exporter 覆盖 KsADK 权威 usage,导致 trace 中 token 数据与运行时不一致的问题。
- 修复 PostgreSQL 恢复后,降级期间创建的 session 无法继续写入 PG 的问题。
- 修复 ADK session 在健康 PostgreSQL 下不刷新其他副本新增事件的问题。
Expand Down
4 changes: 2 additions & 2 deletions docs/maintainer-approval-record.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ Record exactly one approved source publication strategy.
The approved strategy must name the reviewed commit, tag, pull request, or
export archive used for:

- `ksadk-python`: clean export candidate from reviewed internal commit `a40948ea79aed139caf2db17af38bf0032f6a73e`; local candidate directory `/tmp/ksadk-python-export-candidate-0.7.0-a40948e`; verified on 2026-07-15 with 1686 passed / 6 skipped in the full Python suite, successful clean-export `make public-preflight` (version, secret/source audit, 19 public tests, docs build, wheel/sdist, twine and artifact audit), and source/dist audits with 0 violations. Staging E2E evidence is still required before final release.
- `ksadk-web`: npm package `@kingsoftcloud/ksadk-web@0.2.19` from commit `76029d965ccc328a916bc81e8ba66b5e002a6e5b`; Python candidate commit `a40948ea79aed139caf2db17af38bf0032f6a73e`; published by the trusted GitHub npm workflow on 2026-07-15 and consumed from the npm registry during the 0.7.0 candidate verification.
- `ksadk-python`: reviewed internal commit `82646634a88675f0f10ec8c89d2e86e778088079`, including final runtime-name validation for generated projects. The earlier `a40948ea79aed139caf2db17af38bf0032f6a73e` candidate passed its full suite and clean-export preflight; a fresh clean export and complete `make public-preflight` are required for `8264663` before publication.
- `ksadk-web`: npm package `@kingsoftcloud/ksadk-web@0.2.19` from commit `76029d965ccc328a916bc81e8ba66b5e002a6e5b`; paired with Python reviewed commit `82646634a88675f0f10ec8c89d2e86e778088079`; published by the trusted GitHub npm workflow on 2026-07-15.

Both approved source references must include the current commit SHA at approval
time. This prevents a stale approval record from passing after candidate
Expand Down
5 changes: 2 additions & 3 deletions export-manifest.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
{
"generatedAt": "2026-07-15T10:49:30.407984+00:00",
"generatedAt": "2026-07-15T15:04:58.326366+00:00",
"targetRepository": "https://github.com/kingsoftcloud/ksadk-python",
"documentation": "https://kingsoftcloud.github.io/ksadk-python/",
"exportPathCount": 556,
"excludedPathCount": 208,
"excludedPathCount": 207,
"excludedPaths": [
"docs/A2UI-agent驱动UI技术方案.md",
"docs/Agent 开发者上下文接入指南.md",
"docs/DeepAgents说明.md",
"docs/adk-resume-integration-design.md",
Expand Down
41 changes: 34 additions & 7 deletions ksadk/cli/cmd_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

import json
import platform
import re
import shlex
import click
from pathlib import Path
import shutil

from pathlib import Path

import click
import questionary
from ksadk.cli.cmd_config import custom_style
from ksadk.cli.ui import (
Expand Down Expand Up @@ -58,6 +61,32 @@ def _print_quick_start_commands(project_name: str, commands: list[str]) -> None:
print_info(line)


_RUNTIME_AGENT_NAME_MAX_LENGTH = 63


def _normalize_runtime_package_name(project_name: str) -> str:
"""Derive a portable package/runtime name from the project directory name."""
package_name = re.sub(r"[^A-Za-z0-9_]", "_", project_name.replace("-", "_"))
if not package_name:
package_name = "agent"
if not re.match(r"^[A-Za-z_]", package_name):
package_name = f"agent_{package_name}"
if len(package_name) > _RUNTIME_AGENT_NAME_MAX_LENGTH:
raise ValueError(
"生成的 Agent 名称 "
f"'{package_name}' 超过运行时 {_RUNTIME_AGENT_NAME_MAX_LENGTH} 字符限制"
)
return package_name


def _runtime_package_name_or_exit(project_name: str) -> str:
try:
return _normalize_runtime_package_name(project_name)
except ValueError as exc:
print_error(str(exc))
raise SystemExit(1) from exc


TEMPLATES = {
"adk": {
"agent.py": '''"""
Expand Down Expand Up @@ -1062,10 +1091,8 @@ def _write_deepagents_service_adapter(

def _wrap_agent_file(from_agent_path: Path, project_name: str, framework: str, agent_var: str):
"""包装单个 Agent 文件到新项目"""
import re

project_path = Path(project_name)
package_name = project_path.name.replace('-', '_')
package_name = _runtime_package_name_or_exit(project_path.name)

if project_path.exists():
print_error(f"目录 '{project_name}' 已存在")
Expand Down Expand Up @@ -1200,7 +1227,7 @@ def _wrap_agent_directory(from_agent_dir: Path, project_name: str, framework: st
import shutil

project_path = Path(project_name)
package_name = project_path.name.replace('-', '_')
package_name = _runtime_package_name_or_exit(project_path.name)
source_dir_name = from_agent_dir.name

if project_path.exists():
Expand Down Expand Up @@ -1967,7 +1994,7 @@ def create(project_name: str, framework: str, from_agent_path: str):
print_kv("创建项目", project_name)
print_kv("框架", framework)

package_name = project_path.name.replace('-', '_')
package_name = _runtime_package_name_or_exit(project_path.name)
project_path.mkdir(parents=True)
if framework not in {"openclaw", "hermes"}:
(project_path / package_name).mkdir(parents=True)
Expand Down
41 changes: 41 additions & 0 deletions ksadk/runners/adk_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,42 @@ def _append_toolsets_by_key(self, toolsets: list[Any], *, key_attr: str) -> list
added_keys.append(key)
return added_keys

def _invalid_agent_name_load_error(self, exc: Exception) -> ValueError | None:
"""Convert ADK's import-time agent-name validation into an actionable error."""
errors = getattr(exc, "errors", None)
if not callable(errors):
return None
try:
details = errors()
except Exception:
return None
if not isinstance(details, list):
return None
for detail in details:
if not isinstance(detail, Mapping):
continue
location = detail.get("loc")
message = str(detail.get("msg") or "")
name = detail.get("input")
if location not in (("name",), ["name"]) or not isinstance(name, str):
continue
if "valid identifier" not in message.lower():
continue
safe_name = "".join(
char if char.isascii() and (char.isalnum() or char == "_") else "_"
for char in name
)
if not safe_name or safe_name[0].isdigit():
safe_name = f"agent_{safe_name}"
return ValueError(
"ADK Agent 名称不合法: "
f"{name!r}。Google ADK 要求 `Agent(name=...)` 以英文字母或下划线开头,"
"且仅包含英文字母、数字和下划线。"
f"请在 {self.detection_result.entry_point} 中创建 "
f"{self.detection_result.agent_variable} 时改为,例如 {safe_name!r}。"
)
return None

def load_agent(self) -> None:
"""加载 ADK Agent"""
import warnings
Expand Down Expand Up @@ -904,6 +940,11 @@ def load_agent(self) -> None:
raise AttributeError(
f"模块 {module_name} 中未找到 {self.detection_result.agent_variable}"
)
except Exception as exc:
name_error = self._invalid_agent_name_load_error(exc)
if name_error is not None:
raise name_error from exc
raise

# 验证是否为 ADK Agent
if not hasattr(self._agent, "name"):
Expand Down
18 changes: 14 additions & 4 deletions ksadk/runners/langgraph_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,15 @@ async def _latest_state_usage(self, config: dict[str, Any]) -> dict[str, Any]:
return self._extract_usage(values)
return {}

@staticmethod
def _latest_checkpoint_config(config: dict[str, Any]) -> dict[str, Any]:
"""Select the newest state in a resumed thread, not the source checkpoint."""
latest_config = dict(config)
configurable = dict(latest_config.get("configurable") or {})
configurable.pop("checkpoint_id", None)
latest_config["configurable"] = configurable
return latest_config

@staticmethod
def _ambient_context_text(payload: Dict[str, Any]) -> str:
sections: list[str] = []
Expand Down Expand Up @@ -668,14 +677,15 @@ async def _stream_checkpoint_resume_updates(
else:
yield {"type": "graph_update", "output": update}

state_usage = await self._latest_state_usage(config)
latest_config = self._latest_checkpoint_config(config)
state_usage = await self._latest_state_usage(latest_config)
state_output = ""
try:
state = None
if callable(getattr(self._agent, "aget_state", None)):
state = await self._agent.aget_state(config)
state = await self._agent.aget_state(latest_config)
elif callable(getattr(self._agent, "get_state", None)):
state = self._agent.get_state(config)
state = self._agent.get_state(latest_config)
values = getattr(state, "values", None)
if values is not None:
state_output = self._extract_output(values)
Expand All @@ -689,7 +699,7 @@ async def _stream_checkpoint_resume_updates(
**({"resume_noop": True} if not emitted_update else {}),
}

metadata = await self._latest_checkpoint_metadata(config)
metadata = await self._latest_checkpoint_metadata(latest_config)
if metadata:
yield {"type": "checkpoint", "metadata": metadata}

Expand Down
Loading