diff --git a/CHANGELOG.md b/CHANGELOG.md index f93fe3a..cf6dccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 下不刷新其他副本新增事件的问题。 diff --git a/docs/maintainer-approval-record.md b/docs/maintainer-approval-record.md index eb422b4..1e1e7dc 100644 --- a/docs/maintainer-approval-record.md +++ b/docs/maintainer-approval-record.md @@ -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 diff --git a/export-manifest.json b/export-manifest.json index 1602737..0ed7c83 100644 --- a/export-manifest.json +++ b/export-manifest.json @@ -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", diff --git a/ksadk/cli/cmd_create.py b/ksadk/cli/cmd_create.py index 4469c69..8e528b1 100644 --- a/ksadk/cli/cmd_create.py +++ b/ksadk/cli/cmd_create.py @@ -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 ( @@ -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": '''""" @@ -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}' 已存在") @@ -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(): @@ -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) diff --git a/ksadk/runners/adk_runner.py b/ksadk/runners/adk_runner.py index 8e2a15b..beab7e5 100644 --- a/ksadk/runners/adk_runner.py +++ b/ksadk/runners/adk_runner.py @@ -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 @@ -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"): diff --git a/ksadk/runners/langgraph_runner.py b/ksadk/runners/langgraph_runner.py index 65a1105..c0f2e86 100644 --- a/ksadk/runners/langgraph_runner.py +++ b/ksadk/runners/langgraph_runner.py @@ -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] = [] @@ -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) @@ -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}