diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index f913715..c6fdf22 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -13,6 +13,14 @@ on: approved_source_commit: description: Reviewed source commit SHA recorded in docs/maintainer-approval-record.md required: false + publish_target: + description: Publish target + required: false + default: full + type: choice + options: + - full + - alias-only permissions: contents: read @@ -31,6 +39,7 @@ jobs: env: KSADK_WEB_VERSION: ${{ github.event.inputs.ksadk_web_version || '0.2.18' }} KSADK_APPROVED_SOURCE_COMMIT: ${{ github.event.inputs.approved_source_commit || vars.KSADK_APPROVED_SOURCE_COMMIT }} + PUBLISH_TARGET: ${{ github.event.inputs.publish_target || 'full' }} permissions: contents: read id-token: write @@ -58,20 +67,35 @@ jobs: run: make sync-ksadk-web-static - name: Run public release preflight + if: env.PUBLISH_TARGET == 'full' run: make public-preflight + - name: Run alias-only release checks + if: env.PUBLISH_TARGET == 'alias-only' + run: make public-audit public-test public-build-alias-check + - name: Verify release approval gate run: make public-publish-gate - name: Publish to PyPI + if: env.PUBLISH_TARGET == 'full' + uses: pypa/gh-action-pypi-publish@release/v1 + + - name: Build alias distribution + if: env.PUBLISH_TARGET == 'full' + run: make public-build-alias-check + + - name: Publish alias package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist-alias # 发版时同步部署 Fumadocs 文档站到 GitHub Pages, 文档与 PyPI 版本一致。 # 用 if: always() 让文档部署不被 publish 阻塞(workflow_dispatch 重新部署文档时, # publish 会因版本门禁失败(版本未变),但 deploy-pages 仍应运行)。 deploy-pages: needs: publish - if: always() + if: always() && (github.event_name != 'workflow_dispatch' || github.event.inputs.publish_target != 'alias-only') runs-on: ubuntu-latest environment: name: github-pages diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d0fd8e..e9f5377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,40 +9,22 @@ ### 亮点 -- **run 状态双维度字段**:新增 `run_mode`(background/foreground/unknown)和 `run_trigger`(new_run/checkpoint_resume/approval_resume/unknown)两个独立维度字段,区分"怎么跑"和"怎么开始",替代单字段 `run_kind` 的语义错误。后台长任务从 checkpoint 恢复时不再丢失"这是后台任务"的信息,前端可直接消费 `ActiveRunMode` / `ActiveRunTrigger` 判断长任务会话,无需从事件流推断。 -- **checkpoint 可恢复性聚合字段**:`ListSessionCheckpoints` 响应新增 `ResumableTotal` / `HasResumableCheckpoint`,解决 `Total > 0` 不能代表"可恢复"的误判(终态/过期/memory_local checkpoint 会让 Total 非空但不可恢复)。恢复按钮可用性应看 `HasResumableCheckpoint`。 -- **state_delta.active_run 对齐**:ksadk 与 agentengine-server 现都把 `run_mode` / `run_trigger` 写入 `state_delta.active_run`,Session 对象的 `ActiveRunMode` / `ActiveRunTrigger` 由 state 重建,刷新/分享链接/切 session 都能恢复一致状态。 -- **会话事件续订与历史分页收敛**:runtime `ListSessionEvents` / `SubscribeRunEvents` 支持 `AfterSeqId` 增量续订,session backends 支持 `BeforeSeqId` 向前翻页,控制台可从最新窗口进入历史并稳定重连,不再依赖全量扫事件。 -- **真实 token usage 契约补齐**:ADK、LangChain、LangGraph runner 在单轮内累计多次 LLM 调用 usage,同时保留 `last_usage`,让服务端可以同时得到会话累计 token 消耗和最后一轮上下文窗口占用,避免用累计值误当窗口占用。 - -### 新增 - -- 新增 `ksadk/conversations/run_kinds.py`:`run_mode` / `run_trigger` 枚举常量 + `validate_run_mode` / `validate_run_trigger` + `trigger_from_resume_input`(从 resume_input 推导 trigger)。 -- `append_run_status_event` 新增 `run_mode` / `run_trigger` 参数,写入事件 `metadata` 与 `state_delta.active_run`。 -- `PreparedConversationTurn` 新增 `run_mode` / `run_trigger` 字段,`build_run_input` 按 checkpoint_resume / approval_resume / new_run 分支回填。 -- `invoke_conversation_once` / `_iter_conversation_turn_events` / `stream_responses_conversation_turn` / `stream_conversation_turn` 透传 `run_mode`;18+ 处 `append_run_status_event` 调用点按 endpoint 语义传值。 -- 各 endpoint 按产品语义标记 run_mode:`RunAgent Background:true` 与 `ResumeRun Stream:true` 标 `background`;普通 `RunAgent Stream:true`、`ResumeRun Stream:false`、`/v1/responses`、`/v1/chat/completions`、`/run_sse` 标 `foreground`。 -- `_DetachedSSEStream` 构造与终态 fallback 写入 `run_mode` / `run_trigger`。 -- 新增 `_latest_session_run_metadata` helper(不改原 `_latest_session_run_status`,保护现有契约),`_session_to_action_payload` 顶层新增 `ActiveRunMode` / `ActiveRunTrigger`。 -- `_list_checkpoints_payload` 新增 `ResumableTotal` / `HasResumableCheckpoint` 聚合字段。规则:`IsResumable===true && ReplayAllowed!==false && IsTerminal!==true && CheckpointStatus not in {expired, disabled}`,不排除 `resumed`(已恢复过的仍计入,符合存档点可反复读)。 -- agentengine-server 侧新建 `app/services/run_kinds.py`(独立维护,不 import ksadk,用测试约束一致性),`_append_run_status` 与 `_serialize_session` 同步写入/读取新字段。 -- 新增 `ksadk/runners/usage_accumulator.py`,统一归一化并累加 OpenAI/ADK/LangChain/LangGraph usage 字段,覆盖 `input_tokens`、`output_tokens`、`total_tokens` 及 token details。 -- runtime `ListSessionEvents` 新增 `AfterSeqId` / `BeforeSeqId` 过滤能力;`SubscribeRunEvents` 支持 `AfterSeqId`,用于断线后只推送已读序号之后的新事件。 -- `SessionService.get_events()` 在 in-memory、local SQLite、Postgres 后端补齐 `after_seq_id` / `before_seq_id` 过滤,保持最新窗口、向后增量、向前翻页三类语义一致。 +- **控制台会话恢复更稳**:Session 直接返回活跃 run 的结构化状态,刷新、切会话和分享链接进入时不再依赖前端全量扫描事件流来猜测是否需要重连。 +- **checkpoint 恢复按钮更准**:`ListSessionCheckpoints` 返回可恢复 checkpoint 聚合结果,避免把终态、过期或禁用 checkpoint 误判成可恢复。 +- **长会话历史加载更可靠**:事件列表补齐向后增量续订和向前翻页语义,in-memory、SQLite、Postgres 后端保持一致,支持从最新窗口进入再加载更早历史。 +- **token usage 不再少算**:ADK、LangChain、LangGraph runner 会聚合同一轮里的多次模型调用,同时保留最后一次模型调用的 usage,便于服务端同时计算累计消耗和上下文窗口占用。 +- **公开发布门禁收敛**:公开仓 CI、源码审计、产物审计、Trusted Publishing 和 GitHub Pages 部署边界继续收紧,发版依赖 GitHub 可信流水线。 ### 变更 -- `run_status` 事件 `metadata` 与 `state_delta.active_run` 扩展为含 `run_mode` / `run_trigger`;旧 session 缺字段降级 `unknown`,不破坏现有 `ActiveInvocationId` / `ActiveRunStatus` 契约。 -- approval 续跑的 `run_mode` 跟随原 run(不写死 foreground),需从原 run 上下文透传。 -- server 侧 `run_status` 事件 `content` 仍为 `{status, detail}`,`run_mode` / `run_trigger` 只写进 `state_delta`,避免破坏现有消费方。 -- runner 返回 `metadata.usage` 继续表示单次响应的累计真实消耗;新增 `metadata.last_usage` 表示最后一次模型调用的 usage,供上层计算 `ContextUsage` 等窗口占用指标。 -- LangChain / LangGraph 的 final chunk metadata 增加 `usage` 与 `last_usage`,保留原响应内容结构,避免只取最后一个 chunk 或最后一次 LLM 调用造成 token 少算。 +- Session 响应新增活跃 run 的模式和触发来源字段,用于替代从事件流猜测运行形态的前端逻辑。 +- `run_status` 事件继续保持原 `content` 结构,新元数据写入 `metadata` / `state_delta.active_run`,避免破坏旧消费方。 +- Runtime event paging 的语义明确为:`AfterSeqId` 用于向后增量,`BeforeSeqId` 用于加载更早历史。 ### 修复 -- 修复 ADK runner 单轮内多次 LLM 调用时只保留最后一次 usage,导致会话累计 token 消耗少算的问题。 -- 修复 BaseRunner 从响应列表尾部反向取 usage,遇到多段模型调用时无法聚合的问题。 -- 修复 runtime 事件分页只支持 offset/limit,前端重连和历史向上翻页需要额外扫全量事件的问题。 +- 修复多次 LLM 调用只统计最后一次 usage,导致会话累计 token 消耗少算的问题。 +- 修复 runtime 历史分页只靠 offset/limit,长会话首次打开、向上翻页和重连补事件容易错页的问题。 - 修复 `SubscribeRunEvents` hosted/runtime 双链路续订语义不一致,断线重连可能重复消费旧事件的问题。 ## [0.6.8] - 2026-07-03 diff --git a/Makefile b/Makefile index aaba196..20f6a1f 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # AgentEngine Makefile # 用于同步 KsADK Web static 和管理项目 -.PHONY: help install clean clean-cache clean-dist clean-static clean-offline dev test publish publish-test public-status public-init-worktree public-worktree-status public-sync-check public-secret-audit public-audit public-version-gate docs-site-build docs-site-dev public-test public-build-check public-preflight public-publish-check public-release-approval-check public-publish-gate public-release-tag public-review public-sync-ksadk-web-static open-source-audit-dist openclaw-build openclaw-push openclaw-size hermes-build hermes-push hermes-size sync-ksadk-web-static sync-hosted-ui build-frontend build-webui sync-static webui build-wheel build-all clean-frontend +.PHONY: help install clean clean-cache clean-dist clean-static clean-offline dev test publish publish-test public-status public-init-worktree public-worktree-status public-sync-check public-secret-audit public-audit public-version-gate docs-site-build docs-site-dev public-test public-build-check public-build-alias-check public-preflight public-publish-check public-release-approval-check public-publish-gate public-release-tag public-review public-sync-ksadk-web-static open-source-audit-dist open-source-audit-alias-dist openclaw-build openclaw-push openclaw-size hermes-build hermes-push hermes-size sync-ksadk-web-static sync-hosted-ui build-frontend build-webui sync-static webui build-wheel build-all clean-frontend # 默认目标 help: @@ -377,6 +377,13 @@ public-build-check: clean-dist sync-ksadk-web-static @uv run --extra dev python -m twine check dist/* @$(MAKE) open-source-audit-dist +public-build-alias-check: sync-ksadk-web-static + @echo "==> build and twine check alias distribution" + @rm -rf dist-alias + @uv run python scripts/build_alias_distribution.py --alias-project "$(PUBLIC_ALIAS_PYPI_PROJECT)" --out-dir dist-alias + @uv run --extra dev python -m twine check dist-alias/* + @$(MAKE) open-source-audit-alias-dist + open-source-audit-dist: @echo "==> audit wheel and sdist file lists" @if ! ls dist/*.whl dist/*.tar.gz >/dev/null 2>&1; then \ @@ -386,6 +393,15 @@ open-source-audit-dist: @python3 -c 'import glob, zipfile; [print(name) for path in sorted(glob.glob("dist/*.whl")) for name in zipfile.ZipFile(path).namelist()]' | python3 scripts/open_source_audit.py --target wheel --file-list - @python3 -c 'import glob, tarfile; [print(name) for path in sorted(glob.glob("dist/*.tar.gz")) for name in tarfile.open(path).getnames()]' | python3 scripts/open_source_audit.py --target sdist --file-list - +open-source-audit-alias-dist: + @echo "==> audit alias wheel and sdist file lists" + @if ! ls dist-alias/*.whl dist-alias/*.tar.gz >/dev/null 2>&1; then \ + echo "❌ alias dist artifacts missing; run make public-build-alias-check first"; \ + exit 1; \ + fi + @python3 -c 'import glob, zipfile; [print(name) for path in sorted(glob.glob("dist-alias/*.whl")) for name in zipfile.ZipFile(path).namelist()]' | python3 scripts/open_source_audit.py --target wheel --file-list - + @python3 -c 'import glob, tarfile; [print(name) for path in sorted(glob.glob("dist-alias/*.tar.gz")) for name in tarfile.open(path).getnames()]' | python3 scripts/open_source_audit.py --target sdist --file-list - + public-version-gate: @echo "==> release version gate (prevent downgrade/re-publish)" uv run python scripts/check_release_version.py diff --git a/docs/maintainer-approval-record.md b/docs/maintainer-approval-record.md index 18261e2..cbfc37f 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 `eb76b17d7d3c176f7cf6126ceb001ab00f5d651d`; local candidate directory `/tmp/ksadk-python-export-candidate-0.6.9`; verified on 2026-07-08 with public source audit, registry-bundled `make public-preflight` in the public candidate worktree, Fumadocs static build, wheel/sdist build, twine check, and source/dist package audits. -- `ksadk-web`: npm package `@kingsoftcloud/ksadk-web@0.2.18` from commit `24551d0f290e5a4efc5b5d60d02fa298cccd2efa`; Python candidate commit `eb76b17d7d3c176f7cf6126ceb001ab00f5d651d`; published by the trusted GitHub npm workflow on 2026-07-08 and consumed from the npm registry during `make public-preflight`. +- `ksadk-python`: clean export candidate from reviewed internal commit `0ac219f0aea2214647f9a373537d39e27297c0ec`; local candidate directory `/tmp/ksadk-python-export-candidate-0.6.9-alias`; verified on 2026-07-08 with public source audit, release workflow contract tests, alias wheel/sdist build, twine check, and source/dist package audits. This candidate preserves the already-published `ksadk==0.6.9` release and adds trusted GitHub workflow support for publishing the compatibility alias package `agentengine-sdk-python==0.6.9`. +- `ksadk-web`: npm package `@kingsoftcloud/ksadk-web@0.2.18` from commit `24551d0f290e5a4efc5b5d60d02fa298cccd2efa`; Python candidate commit `0ac219f0aea2214647f9a373537d39e27297c0ec`; published by the trusted GitHub npm workflow on 2026-07-08 and consumed from the npm registry during alias distribution verification. Both approved source references must include the current commit SHA at approval time. This prevents a stale approval record from passing after candidate @@ -50,7 +50,7 @@ changes. Hermes/OpenClaw default images, long-task resume, and terminal reconnect are covered by the staging E2E evidence. - GitHub PR checks are green on the reviewed commit. -- Release notes and `CHANGELOG.md` were reviewed. +- Release notes and `CHANGELOG.md` were reviewed, including the concise 0.6.9 summary. - Public README and docs were reviewed for sensitive environment names, internal endpoints, tokens, customer data, and inaccurate competitor claims. - PyPI/TestPyPI credentials stay outside the repository. diff --git a/export-manifest.json b/export-manifest.json index b96167d..d6bf08f 100644 --- a/export-manifest.json +++ b/export-manifest.json @@ -1,8 +1,8 @@ { - "generatedAt": "2026-07-08T04:10:29.360629+00:00", + "generatedAt": "2026-07-08T04:45:14.107433+00:00", "targetRepository": "https://github.com/kingsoftcloud/ksadk-python", "documentation": "https://kingsoftcloud.github.io/ksadk-python/", - "exportPathCount": 555, + "exportPathCount": 556, "excludedPathCount": 201, "excludedPaths": [ "docs/Agent 开发者上下文接入指南.md", @@ -251,6 +251,7 @@ ], "scripts": [ "scripts/audit_release_artifacts.py", + "scripts/build_alias_distribution.py", "scripts/check_approval_record.py", "scripts/check_publication_state.py", "scripts/check_release_version.py", diff --git a/scripts/build_alias_distribution.py b/scripts/build_alias_distribution.py new file mode 100644 index 0000000..c4d4743 --- /dev/null +++ b/scripts/build_alias_distribution.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Build a PyPI alias distribution for the ksadk package. + +The alias package keeps the Python import/package layout as ``ksadk`` and only +changes the distribution metadata name. It is used for the compatibility PyPI +project ``agentengine-sdk-python``. +""" + +from __future__ import annotations + +import argparse +import re +import shutil +import subprocess +import tempfile +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_ALIAS_PROJECT = "agentengine-sdk-python" +DEFAULT_ALIAS_DESCRIPTION = "Kingsoft Cloud Agent Engine SDK alias package for ksadk" + +IGNORED_DIRS = { + ".git", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + ".venv", + ".worktrees", + ".zread", + "__pycache__", + "build", + "dist", + "dist-alias", + "htmlcov", + "ksadk.egg-info", + "node_modules", + "site", +} +IGNORED_SUFFIXES = (".pyc", ".pyo") + + +def _ignore(_dir: str, names: list[str]) -> set[str]: + ignored: set[str] = set() + for name in names: + if name in IGNORED_DIRS or name.endswith(IGNORED_SUFFIXES): + ignored.add(name) + return ignored + + +def _rewrite_pyproject(pyproject: Path, *, alias_project: str, description: str) -> None: + text = pyproject.read_text(encoding="utf-8") + text, name_count = re.subn( + r'(?m)^name = "ksadk"$', + f'name = "{alias_project}"', + text, + count=1, + ) + text, description_count = re.subn( + r'(?m)^description = ".*"$', + f'description = "{description}"', + text, + count=1, + ) + if name_count != 1: + raise RuntimeError("pyproject.toml 中未找到唯一的 ksadk project name") + if description_count != 1: + raise RuntimeError("pyproject.toml 中未找到唯一的 project description") + pyproject.write_text(text, encoding="utf-8") + + +def build_alias_distribution( + *, + alias_project: str, + description: str, + out_dir: Path, +) -> None: + out_dir = out_dir.resolve() + if out_dir.exists(): + shutil.rmtree(out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + + with tempfile.TemporaryDirectory(prefix="ksadk-alias-src-") as tmp: + alias_root = Path(tmp) / "src" + shutil.copytree(ROOT, alias_root, ignore=_ignore) + _rewrite_pyproject( + alias_root / "pyproject.toml", + alias_project=alias_project, + description=description, + ) + subprocess.run( + ["uv", "build", "--out-dir", str(out_dir)], + cwd=alias_root, + check=True, + ) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--alias-project", default=DEFAULT_ALIAS_PROJECT) + parser.add_argument("--description", default=DEFAULT_ALIAS_DESCRIPTION) + parser.add_argument("--out-dir", type=Path, default=ROOT / "dist-alias") + args = parser.parse_args() + + build_alias_distribution( + alias_project=args.alias_project, + description=args.description, + out_dir=args.out_dir, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/prepare_ksadk_python_export.py b/scripts/prepare_ksadk_python_export.py index ae6232c..1f2a4d2 100644 --- a/scripts/prepare_ksadk_python_export.py +++ b/scripts/prepare_ksadk_python_export.py @@ -82,6 +82,7 @@ SCRIPT_EXPORT_FILES = { "scripts/audit_release_artifacts.py", + "scripts/build_alias_distribution.py", "scripts/check_approval_record.py", "scripts/check_publication_state.py", "scripts/check_release_version.py", diff --git a/tests/test_public_release_positioning.py b/tests/test_public_release_positioning.py index a6f6c07..c2f3040 100644 --- a/tests/test_public_release_positioning.py +++ b/tests/test_public_release_positioning.py @@ -172,6 +172,8 @@ def test_pypi_publish_workflow_uses_trusted_publishing_and_bundles_ksadk_web(): assert "release:" in workflow assert "- published" in workflow assert "workflow_dispatch:" in workflow + assert "publish_target:" in workflow + assert "alias-only" in workflow assert 'default: "0.2.18"' in workflow assert "approved_source_commit:" in workflow assert "Reviewed source commit SHA recorded in docs/maintainer-approval-record.md" in workflow @@ -179,7 +181,12 @@ def test_pypi_publish_workflow_uses_trusted_publishing_and_bundles_ksadk_web(): assert "KSADK_APPROVED_SOURCE_COMMIT: ${{ github.event.inputs.approved_source_commit || vars.KSADK_APPROVED_SOURCE_COMMIT }}" in workflow assert "make sync-ksadk-web-static" in workflow assert "make public-preflight" in workflow + assert "make public-audit public-test public-build-alias-check" in workflow assert "make public-publish-gate" in workflow + assert "if: env.PUBLISH_TARGET == 'full'" in workflow + assert "Publish alias package to PyPI" in workflow + assert "packages-dir: dist-alias" in workflow + assert "github.event.inputs.publish_target != 'alias-only'" in workflow assert "make open-source-audit-dist" in ci_workflow assert "make public-test" in ci_workflow assert "tests/test_conversation_runtime.py" not in ci_workflow