diff --git a/AGENTS.md b/AGENTS.md index e1d6e3fe1..428373957 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,7 @@ 3. **结果汇报优先** - 收尾说明做了什么、为什么这样做、验证了什么、还剩什么缺口;避免过程性礼貌汇报 4. **任务完成标准优先** - 以可编译、类型正确、测试通过、功能真实可用作为完成依据;实现细节服从项目既有模式和当前主线目标 5. **不主动扩大承诺** - 不在完成后追问“要不要继续做 X/Y/Z”;如存在自然下一刀,只简短列出建议,等待用户明确要求 +6. **并行协作先切写集** - 用户说明有其他 Agent / 进程同跑时,先只读盘点 `git status --short` 和目标 diff,声明本轮认领的窄写集;只修改认领文件,发现目标文件已被别人改动或出现未确认的未跟踪相邻产物时,立即切到只读审阅 / 验证,不夹写、不覆盖 ## 基础约束 @@ -78,6 +79,7 @@ - **状态 / 历史 / 遥测主链**:`docs/aiprompts/state-history-telemetry.md` - **任务分层 / 模型经济调度路线图**:`docs/roadmap/task/README.md` - **治理与收口**:`docs/aiprompts/governance.md` +- **并行 Agent 协作**:`docs/aiprompts/parallel-agent-collaboration.md` - **Harness Engine 治理**:`docs/aiprompts/harness-engine-governance.md` - **Playwright / GUI 续测**:`docs/aiprompts/playwright-e2e.md` - **计划与进度**:`docs/exec-plans/README.md` diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index bfcaf21f9..25ad3d685 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,7 +1,7 @@ ## Lime v1.41.0 发布日期:`2026-05-16` -递交范围:当前完整 worktree,包含 tracked 与新增文件;本次先按要求提交并推送 GitHub,完整发布门禁将在后续补跑后再更新结论。 +递交范围:当前完整 worktree,包含 tracked 与新增文件;本次已补齐 Cargo / NPM 静态检查、单测、契约检查与 GUI smoke 复核结论。 > 发布说明:上一版 release tag 为 `v1.40.0`。本版升级到 `v1.41.0`,并继续清理旧 `RELEASE_NOTES.md` 历史堆叠内容,只保留当前版本说明;旧 v1.40.0 发布说明不再作为当前 release note 事实源保留。 @@ -13,6 +13,7 @@ - Runtime evidence / handoff / review / replay 导出结果可投影回 Agent App task event,Evidence Pack、analysis、review decision、replay case 等制品不再停留在只读导出侧。 - Claw capability 复用从命令文件中拆出 catalog service,首批 `image / cover / research / report / pdf / summary` capability hint 只映射到既有 Claw launch metadata,不新增垂直 `content_factory_*` 命令。 - 正式 `Agent Apps` GUI smoke 形成独立证据,覆盖正式入口、registration blocker、Cloud install review、lifecycle disable / enable、runtime surface、uninstall rehearsal 和 flag-off regression。 +- 追加 P18.7 full Lime capability surface 更新:Agent App manifest / readiness / host profile / SDK catalog 开始对齐完整 Lime capability surface,Host Bridge 与 runtime dispatcher 不再只覆盖内容工厂最小子集。 ### 用户可见更新 @@ -20,21 +21,31 @@ - 内容工厂 App 可通过 Host Bridge 订阅 AgentRuntime task update,并从 artifact / snapshot / event payload 中回放 `workspacePatch` / `contentFactoryWorkspacePatch`。 - App 内任务结果支持展示 artifact、evidence、verification outcome 与运行进度,减少“任务 accepted 后长期 idle”的误导状态。 - Agent App hidden runtime session 不再污染普通最近对话,任务恢复和 Host response 继续使用 app / task scope。 +- Agent Apps 侧栏与 Runtime 页面补齐正式入口、capability readiness、runtime process 状态和 Host response 可见反馈,减少只在 Lab / mock 路径验证的断层。 +- `@配图` 命令 smoke 改为接受 current 的 `execute_skill` 直连 Skill 执行状态,覆盖 `正在执行 Skill: image_generate` 与图片预览可见性,避免继续等待旧内层 tool process 文案。 ### 开发者与治理更新 - 新增 `src/features/agent-app/sdk/` 相关 facade、contract、adapter 与 public surface 回归,固定 SDK-only consumer 边界。 +- 新增 `src/features/agent-app/sdk/capabilityCatalog.ts`、`src/features/agent-app/runtime/agentRuntimeProcess.ts` 及对应回归,补齐 capability catalog、runtime process lifecycle 与 Host Bridge 分发边界。 - 新增 `scripts/agent-app-package-handoff-check.mjs` 与 `scripts/lib/agent-app-package-handoff-core.mjs`,机械检查外部 Agent App package 的 dirty 状态、私有 bridge marker、SDK marker、高风险脚本和 `src -> dist` 产物漂移。 - 新增 `src-tauri/src/services/agent_app_runtime_capability_catalog_service.rs` 与 `src-tauri/src/services/runtime_evidence_projection_service.rs`,把 capability catalog 与 evidence projection 从 runtime command 里拆成单一职责服务。 +- `agent_app_runtime_cmd` 拆分为模块目录,降低单文件命令膨胀风险,并让 Runtime facade、event projection、Host response、content factory output contract 各自收敛到更清晰的边界。 +- 默认 Skills 追加 `article-writer`、`content-reviewer`、`knowledge-builder` 三类本地包,并补齐默认 Skill 嵌入、同步与 gate 回归。 - `docs/roadmap/agentapp/` 与 `docs/roadmap/agentruntime/` 补齐 P17.5、P18 handoff、package SDK migration、completion audit、raw worker pre-gate 与 AgentRuntime completion audit,当前事实源回到 versioned artifact。 - `agent_app_runtime_cmd` 增加模型偏好解析、最近成功 Agent run fallback、provider catalog fallback、runtime event emit、artifact replay、evidence projection 与 content factory output contract。 ### 当前校验状态 -- `cargo fmt --manifest-path "src-tauri/Cargo.toml" --all`:已通过。 -- `cargo test --manifest-path "src-tauri/Cargo.toml"`:已启动但按用户要求先提交,后台进程已终止;尚未形成通过结论。 -- `npm run verify:app-version`:待本次提交前快速复核。 -- `cargo clippy` / Rust lint、`npm run lint`、`npm test`、`npm run test:contracts`、`npm run verify:gui-smoke`:本次先提交 GitHub,尚未补跑完整结论。 +- `cargo fmt --manifest-path "src-tauri/Cargo.toml" --all`:已通过,并追加 `cargo fmt --manifest-path "src-tauri/Cargo.toml" --all -- --check` 复核通过。 +- `cargo clippy --manifest-path "src-tauri/Cargo.toml" --all-targets --all-features -- -D warnings`:已通过。 +- `cargo test --manifest-path "src-tauri/Cargo.toml"`:已通过;主库 `1347 passed / 1 ignored`,集成测试通过,真实联网测试按环境变量保持 ignored。 +- `npm run lint`:已通过。 +- `npm test`:已通过;`run-vitest-smart` 全部 `58/58` 批次通过。 +- `npm run verify:app-version`:已通过,版本一致性为 `1.41.0`。 +- `npm run test:contracts`:已通过,覆盖 agent runtime client 生成检查、命令契约、harness 契约、modality runtime contract 与 cleanup report contract。 +- `npm run smoke:at-command-registry -- --app-url http://127.0.0.1:1420/ --health-url http://127.0.0.1:3030/health --invoke-url http://127.0.0.1:3030/invoke --timeout-ms 180000 --interval-ms 1000`:已通过;`@配图` current 路由为 `execute_skill`,可见状态为 `正在执行 Skill: image_generate`,图片预览可见。 +- `npm run verify:gui-smoke`:已执行;workspace ready、browser runtime、site adapters、agent service skill entry、runtime tool surface、runtime surface page、at-command registry、Agent Apps smoke 均已通过;最终在 `smoke:claw-chat-ready-streaming` 的 live recovery turn 等待完成阶段超时,单独复跑同样停在 `等待恢复 turn 完成`,证据已写入 `.lime/qc/gui-evidence/claw-chat-ready-streaming/`。 --- diff --git a/docs/aiprompts/README.md b/docs/aiprompts/README.md index 960c9e59c..d5fcb6782 100644 --- a/docs/aiprompts/README.md +++ b/docs/aiprompts/README.md @@ -24,6 +24,7 @@ - `persistence-map.md` - Runtime 文件快照持久化主链、artifact sidecar/version/checkpoint 边界 - `state-history-telemetry.md` - State / History / Telemetry current 主链、session/thread/request/evidence/history 边界与 current/compat 分类 - `governance.md` - 新旧并存治理、迁移收口、禁止回流 +- `parallel-agent-collaboration.md` - 多 Agent / 多终端并行开发时的写集声明、避让和冲突处理协议 - `harness-engine-governance.md` - Harness Engine 事实源、evidence pack、replay / analysis / review 治理规范 - `quality-workflow.md` - 本地校验、GUI smoke、契约检查、CI 门禁 - `../tests/agent-ops-qc.md` - Agent 运营级测试体系、qcloop 场景、Evidence Pack 与发布证据门禁 @@ -82,6 +83,7 @@ - **做网页登录态访问 / 网页导出 / Markdown 落盘场景**:先读 `web-browser-scene-skill.md` - **改 Workspace / GUI 壳 / 主路径**:先读 `workspace.md`、`quality-workflow.md`、`playwright-e2e.md` - **做迁移 / 收口 / 去兼容层**:先读 `governance.md` +- **多个 Agent / 终端同跑一个任务**:先读 `parallel-agent-collaboration.md`,再声明本轮写集 - **改 handoff / evidence pack / replay / review / HarnessStatusPanel**:先读 `state-history-telemetry.md`,再看 `harness-engine-governance.md` 与 `governance.md` - **改 Provider / 凭证加载 / Token 刷新**:先读 `providers.md`、`credential-pool.md` - **做跨仓库联动**:先读 `limecore-collaboration-entry.md` diff --git a/docs/aiprompts/parallel-agent-collaboration.md b/docs/aiprompts/parallel-agent-collaboration.md new file mode 100644 index 000000000..da1ded125 --- /dev/null +++ b/docs/aiprompts/parallel-agent-collaboration.md @@ -0,0 +1,28 @@ +# 并行 Agent 协作 + +本文件约束多个 Agent / 终端进程同时开发 Lime 仓库时的协作方式。目标是减少猜测、避免夹写,并让每个进程都能明确知道自己能做什么。 + +## 启动协议 + +1. **先确认主线** - 用一句话复述当前路线图目标、阶段和本轮最小交付。 +2. **先只读盘点** - 在目标范围内执行 `git status --short -- ` 和 `git diff --name-only -- `;不要先改文件。 +3. **声明写集** - 在动手前明确说出“我认领哪些文件 / 目录”“我只读哪些文件 / 目录”“我不碰哪些文件 / 目录”。 +4. **写集要窄** - 优先按垂直切片认领最少文件;不能用“整个 `src/features/agent-app`”替代真实写集,除非任务确实需要。 +5. **测试可共享** - 定向测试、`typecheck`、`git diff --check` 可以由任一进程执行,但报告时要说明它验证的是当前工作树,不代表改动归属。 + +## 冲突处理 + +1. **目标文件已脏时不夹写** - 如果目标文件已经被其他进程修改,默认切到只读审阅 / 验证;除非用户明确要求接管该文件。 +2. **未跟踪相邻产物先确认** - 发现未确认的未跟踪文档、源码或生成物时,不合并、不删除、不改名,先说明风险和可选处理。 +3. **必须改同一文件时先汇报补丁点** - 如果主线必须触碰对方写集,先给出文件、函数、最小补丁意图和验证命令,等待合并窗口。 +4. **发现意外变化立即停下** - 修改过程中若同一文件出现非本人改动,停止写入并询问如何继续;不要用 checkout / reset 抹平。 +5. **不要抢占外部仓库** - 跨仓库联动时,除非用户明确授权当前进程负责外部仓库,否则只读外部仓库并把需要的变更写成 handoff。 + +## 汇报格式 + +并行任务收尾时必须补充: + +- **本轮认领写集**:列出实际修改的文件。 +- **避让写集**:列出发现但没有触碰的并行文件或未跟踪产物。 +- **验证口径**:说明跑过的命令验证了哪些边界。 +- **下一刀归属**:说明下一步适合由当前进程做,还是应交给正在持有相关写集的进程。 diff --git a/docs/images/lime-claw-readme.png b/docs/images/lime-claw-readme.png deleted file mode 100644 index 8104cf430..000000000 Binary files a/docs/images/lime-claw-readme.png and /dev/null differ diff --git a/docs/roadmap/agentapp/README.md b/docs/roadmap/agentapp/README.md index 84b62e56b..9925f18cf 100644 --- a/docs/roadmap/agentapp/README.md +++ b/docs/roadmap/agentapp/README.md @@ -10,7 +10,7 @@ 服务端 / Cloud / LimeCore 的职责不写在本目录;对应文档在 `/Users/coso/Documents/dev/ai/limecloud/limecore/docs/roadmap/agentapp`。 -产品边界必须固定:Agent App 是业务工作台,Lime Agent 是 App 可编排的智能运行时。用户应该在 App 内完成业务流程,而不是为了生成、确认、交付或复盘被迫跳回 Lime 通用 Chat UI。Chat / Expert 只是 App 的一种入口或嵌入式协作方式,不是业务流程的唯一容器。 +产品边界必须固定:Agent App 是业务工作台,Lime Agent 是 App 可编排的智能运行时。用户应该在 App 内完成业务流程,而不是为了生成、确认、交付或复盘被迫跳回 Lime 通用 Chat UI。Lime Experts 是对话优先的专家模块,适合问答、咨询、分析和轻量任务;Chat / Expert 只是 Agent App 的一种入口或嵌入式协作方式,不是业务流程的唯一容器。 ```text Lime Cloud / LimeCore @@ -25,89 +25,91 @@ Agent App Runtime Package ## 当前事实源 -| 分类 | 对象 | 说明 | -|---|---|---| -| current | `/Users/coso/Documents/dev/ai/limecloud/agentapp` | Agent App v0.4 标准事实源;v0.3 覆盖 executable package / schema / reference CLI,v0.4 固定 Host Bridge v1 运行时事件协议。 | -| current | `docs/roadmap/agentapp/capability-sdk.md` | Lime Desktop 侧 Capability SDK 与 Host Bridge 方案。 | -| current | `docs/roadmap/agentapp/implementation-plan.md` | 客户端落地实施方案,覆盖 P0-P18 当前计划;P17.5 formal entry GUI smoke 已通过独立验证链路,P18 进入 Typed Capability SDK Gate,当前阻塞点是 P18.5.3 package-side SDK facade / verify。 | -| current | `docs/roadmap/agentapp/p0-technical-design.md` | P0 只读 App Host 技术设计,覆盖 manifest、projection、readiness、cleanup dry-run。 | -| current | `docs/roadmap/agentapp/p1-mock-capability-host.md` | P1 Mock Capability Host 技术设计,覆盖 SDK facade、mock artifact/evidence、uninstall delete-data。 | -| current | `docs/roadmap/agentapp/p2-adapter-capability-host.md` | P2 Adapter Capability Host 技术设计,覆盖本地 adapter store、knowledge / agent adapter、provenance 查询、delete-data 清理。 | -| current | `docs/roadmap/agentapp/p3-ui-extension-host.md` | P3 UI Extension Host 技术设计,覆盖受控 UI Host、sandbox、injected SDK bridge、Lab 预览。 | -| current | `docs/roadmap/agentapp/p4-content-factory-demo.md` | P4 内容工厂最小闭环,覆盖项目、知识、内容场景、内容资产、Artifact、Evidence、cleanup。 | -| current | `docs/roadmap/agentapp/p4-workflow-runtime.md` | P4.2 受控 Workflow Runtime,覆盖白名单 DSL、runtime policy、trace、cancel 和内容工厂 demo 迁移。 | -| current | `docs/roadmap/agentapp/content-factory-app.md` | 内容工厂作为客户端标杆案例的 UI / storage / workflow / artifact 方案。 | -| current | `docs/roadmap/agentapp/v0.3-rebaseline-plan.md` | v0.3 / 内容工厂升级后的实施重排计划,记录 P4-R rebaseline 与验证证据。 | -| current | `docs/roadmap/agentapp/p5-cloud-bootstrap.md` | P5 Cloud Bootstrap 客户端接入计划,覆盖 payload 校验、企业定制注册码、统一 package source、readiness 合并和 disable / offline / upgrade 回归。 | -| current | `docs/roadmap/agentapp/p6-schema-coverage.md` | P6 v0.3 projection / readiness schema coverage,覆盖 services、workflows、skills、tools、evals、secrets、overlays、lifecycle 和 setup checks。 | -| current | `docs/roadmap/agentapp/p7-schema-gate.md` | P7 本地 schema / snapshot gate,机械验证 projection / readiness 结构与 setup issue 字段。 | -| current | `docs/roadmap/agentapp/p8-setup-resolver.md` | P8 setup resolver / needs-setup 语义,把结构性 setup checks 推进到明确 readiness 状态。 | -| current | `docs/roadmap/agentapp/p9-setup-state-store.md` | P9 installed setup state store,把 setup resolver 输入推进为本地可查询、可清理的 binding 状态。 | -| current | `docs/roadmap/agentapp/p10-installed-state-persistence.md` | P10 installed app state snapshot,把 preview、setup、projection、readiness 汇总为可清理的可序列化安装状态。 | -| current | `docs/roadmap/agentapp/p11-local-persistence-adapter.md` | P11 Local Persistence Adapter,把 installed state 从 in-memory 推进到本地可恢复 persistence adapter。 | -| current | `docs/roadmap/agentapp/p12-package-cache-verify-rollback.md` | P12 Package Cache / Verify / Rollback,覆盖 package cache、hash verify、cached fallback、upgrade staging 和 rollback。 | -| current | `docs/roadmap/agentapp/p13-runtime-package-loader.md` | P13 Runtime Package Loader / UI Bundle Loader,覆盖已验证 package 的 runtime descriptor 和 UI bundle descriptor 加载边界。 | -| current | `docs/roadmap/agentapp/p14-entry-runtime-guard-permission-prompt.md` | P14 Entry Runtime Guard / Permission Prompt 实现,合并 entry readiness、permission、setup state、package verification 和 runtime policy。 | -| current | `docs/roadmap/agentapp/p15-lab-install-launch-flow.md` | P15 Lab Install / Launch Flow 实现,把安装审查、权限确认、启动和清理串成 Lab-only 端到端流程。 | -| current | `docs/roadmap/agentapp/p15-h-gui-smoke-cleanup-rehearsal.md` | P15-H GUI smoke / cleanup rehearsal hardening,记录 Lab 专用 smoke、证据输出、清理演练和 P16 入口 gate。 | -| current | `docs/roadmap/agentapp/p16-agent-app-manager-product-entry-gate.md` | P16 Agent App Manager / Product Entry Gate,记录实验岛内 App 管理面、entry launcher、lifecycle action、cleanup evidence 与剩余 P16-H 缺口。 | -| current | `docs/roadmap/agentapp/p16-h-multi-app-repository-lifecycle-hardening.md` | P16-H Multi-app repository / lifecycle hardening,记录多 App list、持久化 lifecycle、cleanup evidence export、residual audit 和 P17 前 gate。 | -| current | `docs/roadmap/agentapp/p17-formal-entry-gate-audit.md` | P17 正式入口前 Gate 审计,逐项映射 P16-H 证据、正式 Agent Apps 入口条件和剩余禁区。 | -| current | `docs/roadmap/agentapp/p17-formal-entry-contract.md` | P17.0 正式入口契约,固定 `agent-apps`、`agent-app-lab` 与 runtime surface 的职责边界、禁区、架构和后续 gates。 | -| current | `docs/roadmap/agentapp/p17-source-install-contract-hardening.md` | P17.2 Source / Install Contract Hardening 当前执行计划,固定 source state、install review、registration、Cloud release metadata、package verify、cached fallback 与 reference CLI cross-check 边界。 | -| current | `docs/roadmap/agentapp/p17-lifecycle-cleanup-contract-hardening.md` | P17.3 Lifecycle / Cleanup Contract Hardening 已完成执行计划,固定正式入口 enable / disable、uninstall rehearsal、cleanup evidence、residual audit 与 namespace 归属边界。 | -| current | `docs/roadmap/agentapp/p17-4-host-bridge-runtime.md` | P17.4-H Host Bridge Runtime 已完成执行计划,固定正式 App iframe 与 Lime Host 的 `lime.agentApp.bridge` 事件协议、主题同步、Host action、capability invoke、App-scoped Agent task 与结构化写回边界;P17.4.5 已通过完整 GUI smoke。 | -| current | `docs/roadmap/agentapp/p17-5-formal-entry-gui-smoke.md` | P17.5 Formal Entry GUI Smoke 已完成,记录正式 `agent-apps` 入口 install / registration / launch / disable / uninstall rehearsal / runtime surface / flag-off 的独立 smoke 证据。 | -| current | `docs/roadmap/agentapp/p18-typed-capability-sdk-gate.md` | P18 Typed Capability SDK Gate 计划,固定上游 v0.4 Host Bridge、SDK typed facade、stable error、mock / contract tests 与 App-scoped Agent task 边界;P18.1 SDK contract、P18.2 Host Bridge typed router、P18.3 Core capability adapters、P18.4 App-scoped Agent task SDK facade、P18.4-H handoff gate、P18.5.1 Lime-side SDK regression、P18.5.2 package-side read-only tests、P18.5-S Host Bridge SDK client 与 P18.6 raw worker 前 gate 已落地;2026-05-16 09:22 审计确认下一刀仍是 P18.5.3 package-side SDK facade / verify。 | -| current | `docs/roadmap/agentapp/p18-4-h-agentruntime-handoff-gate.md` | P18.4-H AgentRuntime Handoff Gate,记录 prompt-to-artifact checklist、隔壁 AgentRuntime current MVP 证据、P18 消费侧判定和仍归 AgentRuntime owner 的缺口。 | -| current | `docs/roadmap/agentapp/p18-5-content-factory-sdk-regression.md` | P18.5 内容工厂 SDK 化回归,记录 Lime-side content factory SDK contract、Host Bridge SDK client、外部 package read-only tests、dirty 边界和 package-side 剩余缺口。 | -| current | `docs/roadmap/agentapp/p18-5-3-package-sdk-migration-plan.md` | P18.5.3 package-side SDK facade 迁移计划,固定外部 `content-factory-app` 当前只读事实、推荐写集、迁移步骤、验收清单和回滚点。 | -| current | `docs/roadmap/agentapp/p18-5-3-owner-handoff.md` | P18.5.3 给外部 package owner 的执行单页,浓缩接管前命令、最小写集、禁止项、验证顺序和完成条件。 | -| current | `docs/roadmap/agentapp/p18-6-raw-worker-pre-gate.md` | P18.6 Raw Worker 前 Gate,记录 P18 不执行 raw worker / 外部代码 / 网络 / 文件系统 / secret value 的证据和 P19 后续退出条件。 | -| current | `docs/roadmap/agentapp/p18-completion-audit.md` | P18 completion audit / 协作防打架记录,映射 P18.1-P18.6 证据、外部 package 阻塞、完整 `verify:local` 已通过、2026-05-16 09:04 typecheck / contracts 复核、09:09 feature island 复核和下一刀判定。 | -| reference | `/Users/coso/Documents/dev/ai/limecloud/limecore/docs/roadmap/agentapp` | 服务端 catalog、release、tenant enablement、gateway、ToolHub 路线图;LimeCore 只做 control-plane metadata,真实 release 必须提供 `https` package URL 和完整 package / manifest hash,未激活注册码时不得下发可安装 package。 | +| 分类 | 对象 | 说明 | +| --------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| current | `/Users/coso/Documents/dev/ai/limecloud/agentapp` | Agent App 标准事实源;v0.4 固定 Host Bridge v1,v0.5 固定分层 manifest,v0.6 新增 `app.runtime.yaml` 与 Agent task runtime contract。Lime 已按 P18.7-B 跟上当前 v0.6,不再假定 reference sample 永远停在 v0.3/v0.4/v0.5。 | +| current | `docs/roadmap/agentapp/capability-sdk.md` | Lime Desktop 侧 Capability SDK 与 Host Bridge 方案。 | +| current | `docs/roadmap/agentapp/p18-7-full-lime-capability-surface.md` | P18.7 全量 Lime capability surface:把 UI、数据、AgentRuntime、模型、Token、Skills、Tools、MCP、浏览器、搜索、媒体、终端、记忆、凭证、策略、Evidence 等统一抽象为 `lime.*` 能力,并固定 App / Lime 主 App 边界与 v0.6 分层 manifest / runtime contract 兼容顺序。 | +| current | `docs/roadmap/agentapp/implementation-plan.md` | 客户端落地实施方案,覆盖 P0-P18 当前计划;P18.7-B Agent App v0.6 current compatibility 已完成,当前下一刀是 P18.7-C Host capability discovery。 | +| current | `docs/roadmap/agentapp/p0-technical-design.md` | P0 只读 App Host 技术设计,覆盖 manifest、projection、readiness、cleanup dry-run。 | +| current | `docs/roadmap/agentapp/p1-mock-capability-host.md` | P1 Mock Capability Host 技术设计,覆盖 SDK facade、mock artifact/evidence、uninstall delete-data。 | +| current | `docs/roadmap/agentapp/p2-adapter-capability-host.md` | P2 Adapter Capability Host 技术设计,覆盖本地 adapter store、knowledge / agent adapter、provenance 查询、delete-data 清理。 | +| current | `docs/roadmap/agentapp/p3-ui-extension-host.md` | P3 UI Extension Host 技术设计,覆盖受控 UI Host、sandbox、injected SDK bridge、Lab 预览。 | +| current | `docs/roadmap/agentapp/p4-content-factory-demo.md` | P4 内容工厂最小闭环,覆盖项目、知识、内容场景、内容资产、Artifact、Evidence、cleanup。 | +| current | `docs/roadmap/agentapp/p4-workflow-runtime.md` | P4.2 受控 Workflow Runtime,覆盖白名单 DSL、runtime policy、trace、cancel 和内容工厂 demo 迁移。 | +| current | `docs/roadmap/agentapp/content-factory-app.md` | 内容工厂作为客户端标杆案例的 UI / storage / workflow / artifact 方案。 | +| current | `docs/roadmap/agentapp/v0.3-rebaseline-plan.md` | v0.3 / 内容工厂升级后的实施重排计划,记录 P4-R rebaseline 与验证证据。 | +| current | `docs/roadmap/agentapp/p5-cloud-bootstrap.md` | P5 Cloud Bootstrap 客户端接入计划,覆盖 payload 校验、企业定制注册码、统一 package source、readiness 合并和 disable / offline / upgrade 回归。 | +| current | `docs/roadmap/agentapp/p6-schema-coverage.md` | P6 v0.3 projection / readiness schema coverage,覆盖 services、workflows、skills、tools、evals、secrets、overlays、lifecycle 和 setup checks。 | +| current | `docs/roadmap/agentapp/p7-schema-gate.md` | P7 本地 schema / snapshot gate,机械验证 projection / readiness 结构与 setup issue 字段。 | +| current | `docs/roadmap/agentapp/p8-setup-resolver.md` | P8 setup resolver / needs-setup 语义,把结构性 setup checks 推进到明确 readiness 状态。 | +| current | `docs/roadmap/agentapp/p9-setup-state-store.md` | P9 installed setup state store,把 setup resolver 输入推进为本地可查询、可清理的 binding 状态。 | +| current | `docs/roadmap/agentapp/p10-installed-state-persistence.md` | P10 installed app state snapshot,把 preview、setup、projection、readiness 汇总为可清理的可序列化安装状态。 | +| current | `docs/roadmap/agentapp/p11-local-persistence-adapter.md` | P11 Local Persistence Adapter,把 installed state 从 in-memory 推进到本地可恢复 persistence adapter。 | +| current | `docs/roadmap/agentapp/p12-package-cache-verify-rollback.md` | P12 Package Cache / Verify / Rollback,覆盖 package cache、hash verify、cached fallback、upgrade staging 和 rollback。 | +| current | `docs/roadmap/agentapp/p13-runtime-package-loader.md` | P13 Runtime Package Loader / UI Bundle Loader,覆盖已验证 package 的 runtime descriptor 和 UI bundle descriptor 加载边界。 | +| current | `docs/roadmap/agentapp/p14-entry-runtime-guard-permission-prompt.md` | P14 Entry Runtime Guard / Permission Prompt 实现,合并 entry readiness、permission、setup state、package verification 和 runtime policy。 | +| current | `docs/roadmap/agentapp/p15-lab-install-launch-flow.md` | P15 Lab Install / Launch Flow 实现,把安装审查、权限确认、启动和清理串成 Lab-only 端到端流程。 | +| current | `docs/roadmap/agentapp/p15-h-gui-smoke-cleanup-rehearsal.md` | P15-H GUI smoke / cleanup rehearsal hardening,记录 Lab 专用 smoke、证据输出、清理演练和 P16 入口 gate。 | +| current | `docs/roadmap/agentapp/p16-agent-app-manager-product-entry-gate.md` | P16 Agent App Manager / Product Entry Gate,记录实验岛内 App 管理面、entry launcher、lifecycle action、cleanup evidence 与剩余 P16-H 缺口。 | +| current | `docs/roadmap/agentapp/p16-h-multi-app-repository-lifecycle-hardening.md` | P16-H Multi-app repository / lifecycle hardening,记录多 App list、持久化 lifecycle、cleanup evidence export、residual audit 和 P17 前 gate。 | +| current | `docs/roadmap/agentapp/p17-formal-entry-gate-audit.md` | P17 正式入口前 Gate 审计,逐项映射 P16-H 证据、正式 Agent Apps 入口条件和剩余禁区。 | +| current | `docs/roadmap/agentapp/p17-formal-entry-contract.md` | P17.0 正式入口契约,固定 `agent-apps`、`agent-app-lab` 与 runtime surface 的职责边界、禁区、架构和后续 gates。 | +| current | `docs/roadmap/agentapp/p17-source-install-contract-hardening.md` | P17.2 Source / Install Contract Hardening 当前执行计划,固定 source state、install review、registration、Cloud release metadata、package verify、cached fallback 与 reference CLI cross-check 边界。 | +| current | `docs/roadmap/agentapp/p17-lifecycle-cleanup-contract-hardening.md` | P17.3 Lifecycle / Cleanup Contract Hardening 已完成执行计划,固定正式入口 enable / disable、uninstall rehearsal、cleanup evidence、residual audit 与 namespace 归属边界。 | +| current | `docs/roadmap/agentapp/p17-4-host-bridge-runtime.md` | P17.4-H Host Bridge Runtime 已完成执行计划,固定正式 App iframe 与 Lime Host 的 `lime.agentApp.bridge` 事件协议、主题同步、Host action、capability invoke、App-scoped Agent task 与结构化写回边界;P17.4.5 已通过完整 GUI smoke。 | +| current | `docs/roadmap/agentapp/p17-5-formal-entry-gui-smoke.md` | P17.5 Formal Entry GUI Smoke 已完成,记录正式 `agent-apps` 入口 install / registration / launch / disable / uninstall rehearsal / runtime surface / flag-off 的独立 smoke 证据。 | +| current | `docs/roadmap/agentapp/p18-typed-capability-sdk-gate.md` | P18 Typed Capability SDK Gate 计划,固定上游 Host Bridge、SDK typed facade、stable error、mock / contract tests 与 App-scoped Agent task 边界;P18.1-P18.6 已落地,P18.7-B 已对齐上游 `manifestVersion: 0.6.0`、分层 manifest 与 `app.runtime.yaml` reference sample,下一刀进入 P18.7-C Host capability discovery。 | +| current | `docs/roadmap/agentapp/p18-4-h-agentruntime-handoff-gate.md` | P18.4-H AgentRuntime Handoff Gate,记录 prompt-to-artifact checklist、隔壁 AgentRuntime current MVP 证据、P18 消费侧判定和仍归 AgentRuntime owner 的缺口。 | +| current | `docs/roadmap/agentapp/p18-5-content-factory-sdk-regression.md` | P18.5 内容工厂 SDK 化回归,记录 Lime-side content factory SDK contract、Host Bridge SDK client、外部 package read-only tests、dirty 边界和 package-side 剩余缺口。 | +| current | `docs/roadmap/agentapp/p18-5-3-package-sdk-migration-plan.md` | P18.5.3 package-side SDK facade 迁移计划,固定外部 `content-factory-app` 当前只读事实、推荐写集、迁移步骤、验收清单和回滚点。 | +| current | `docs/roadmap/agentapp/p18-5-3-owner-handoff.md` | P18.5.3 给外部 package owner 的执行单页,浓缩接管前命令、最小写集、禁止项、验证顺序和完成条件。 | +| current | `docs/roadmap/agentapp/p18-6-raw-worker-pre-gate.md` | P18.6 Raw Worker 前 Gate,记录 P18 不执行 raw worker / 外部代码 / 网络 / 文件系统 / secret value 的证据和 P19 后续退出条件。 | +| current | `docs/roadmap/agentapp/p18-completion-audit.md` | P18 completion audit / 协作防打架记录,映射 P18.1-P18.7 证据、外部 package 边界、验证命令、feature island 复核和下一刀判定。 | +| reference | `/Users/coso/Documents/dev/ai/limecloud/limecore/docs/roadmap/agentapp` | 服务端 catalog、release、tenant enablement、gateway、ToolHub 路线图;LimeCore 只做 control-plane metadata,真实 release 必须提供 `https` package URL 和完整 package / manifest hash,未激活注册码时不得下发可安装 package。 | ## 文档索引 -| 文档 | 说明 | -|---|---| -| [capability-sdk.md](./capability-sdk.md) | 客户端 Capability SDK、runtime bridge、能力注入、权限拦截和 mock host。 | -| [implementation-plan.md](./implementation-plan.md) | 客户端 App Host 落地实施方案:manifest parser、projection、installer、storage、UI host、worker runtime。 | -| [p0-technical-design.md](./p0-technical-design.md) | P0 技术设计:只读 package、manifest normalize、projection、readiness、cleanup dry-run、Lab 展示。 | -| [p1-mock-capability-host.md](./p1-mock-capability-host.md) | P1 技术设计:SDK facade、MockCapabilityHost、mock artifact/evidence、uninstall delete-data。 | -| [p2-adapter-capability-host.md](./p2-adapter-capability-host.md) | P2 技术设计:AdapterCapabilityHost、本地 adapter store、knowledge / agent adapter、provenance 查询、delete-data 清理。 | -| [p3-ui-extension-host.md](./p3-ui-extension-host.md) | P3 技术设计:受控 UI Host、sandbox policy、injected SDK bridge、Lab 预览。 | -| [p4-content-factory-demo.md](./p4-content-factory-demo.md) | P4 技术设计:内容工厂最小闭环、content table Artifact、Evidence、delete-data 清理。 | -| [p4-workflow-runtime.md](./p4-workflow-runtime.md) | P4.2 技术设计:受控 workflow runtime、白名单 DSL、trace、cancel、policy guard。 | -| [content-factory-app.md](./content-factory-app.md) | 内容工厂 Product-level Agent App 的客户端产品与实现方案。 | -| [v0.3-rebaseline-plan.md](./v0.3-rebaseline-plan.md) | Agent App v0.3 / 内容工厂升级后的 rebaseline 实施计划。 | -| [p5-cloud-bootstrap.md](./p5-cloud-bootstrap.md) | P5 技术计划:Cloud bootstrap 作为客户端 package source 输入,继续保持本地 install / projection / readiness / runtime / cleanup。 | -| [p6-schema-coverage.md](./p6-schema-coverage.md) | P6 技术计划:补齐 v0.3 projection / readiness schema coverage,但不进入正式主路径。 | -| [p7-schema-gate.md](./p7-schema-gate.md) | P7 技术计划:用本地 schema / snapshot gate 防止 projection / readiness 字段漂移。 | -| [p8-setup-resolver.md](./p8-setup-resolver.md) | P8 技术计划:增加 setup resolver 输入和 `needs-setup` readiness 语义。 | -| [p9-setup-state-store.md](./p9-setup-state-store.md) | P9 技术计划:本地 setup state store、readiness 输入和 cleanup preview 集成。 | -| [p10-installed-state-persistence.md](./p10-installed-state-persistence.md) | P10 技术计划:installed state snapshot、in-memory store、cleanup preview 集成。 | -| [p11-local-persistence-adapter.md](./p11-local-persistence-adapter.md) | P11 技术实现:本地 persistence adapter、启动恢复、坏文件隔离、delete-data 清理。 | -| [p12-package-cache-verify-rollback.md](./p12-package-cache-verify-rollback.md) | P12 技术实现:package cache、hash verify、cached fallback、upgrade staging 和 rollback。 | -| [p13-runtime-package-loader.md](./p13-runtime-package-loader.md) | P13 技术实现:runtime package loader、bundle manifest gate、UI host 接线前安全边界。 | -| [p14-entry-runtime-guard-permission-prompt.md](./p14-entry-runtime-guard-permission-prompt.md) | P14 技术计划:entry runtime guard、permission prompt descriptor、setup / readiness / policy 合并。 | -| [p15-lab-install-launch-flow.md](./p15-lab-install-launch-flow.md) | P15 技术实现:Lab install / launch flow、install review、permission prompt、runtime launch、cleanup preview。 | -| [p15-h-gui-smoke-cleanup-rehearsal.md](./p15-h-gui-smoke-cleanup-rehearsal.md) | P15-H 技术实现:Agent App Lab 专用 GUI smoke、证据输出、cleanup rehearsal 与 P16 gate。 | -| [p16-agent-app-manager-product-entry-gate.md](./p16-agent-app-manager-product-entry-gate.md) | P16 技术计划:Agent App Manager、entry launcher、lifecycle actions、cleanup evidence 和正式入口前 gate。 | -| [p16-h-multi-app-repository-lifecycle-hardening.md](./p16-h-multi-app-repository-lifecycle-hardening.md) | P16-H 技术计划:多 App repository list、持久化 lifecycle、cleanup evidence export、residual audit 和 P17 前 gate。 | -| [p17-formal-entry-gate-audit.md](./p17-formal-entry-gate-audit.md) | P17 Gate 审计:正式入口前 checklist、验证记录、进入条件和 P17.0 契约入口。 | -| [p17-formal-entry-contract.md](./p17-formal-entry-contract.md) | P17.0 技术计划:正式 `agent-apps` 入口契约、Lab 边界、runtime surface、架构 / 时序 / 流程、后续 gates。 | -| [p17-source-install-contract-hardening.md](./p17-source-install-contract-hardening.md) | P17.2 技术计划:正式入口 source state、install review、registration、Cloud release metadata 与客户端安装边界。 | -| [p17-lifecycle-cleanup-contract-hardening.md](./p17-lifecycle-cleanup-contract-hardening.md) | P17.3 技术计划:正式入口 lifecycle、cleanup rehearsal、evidence export、residual audit 与 namespace 分离。 | -| [p17-4-host-bridge-runtime.md](./p17-4-host-bridge-runtime.md) | P17.4-H 技术计划:Host Bridge v1、主题同步、Host action、capability invoke、iframe message 安全边界。 | -| [p17-5-formal-entry-gui-smoke.md](./p17-5-formal-entry-gui-smoke.md) | P17.5 验证记录:正式 `agent-apps` 入口独立 GUI smoke、cleanup dry-run evidence 与 flag-off regression。 | -| [p18-typed-capability-sdk-gate.md](./p18-typed-capability-sdk-gate.md) | P18 技术计划:Typed Capability SDK Gate、Host Bridge v1 调用信封、stable error、mock / contract tests、App-scoped Agent task。 | -| [p18-4-h-agentruntime-handoff-gate.md](./p18-4-h-agentruntime-handoff-gate.md) | P18.4-H 验证记录:AgentRuntime handoff checklist、证据映射、剩余 owner 缺口与 P18.5 进入判定。 | -| [p18-5-content-factory-sdk-regression.md](./p18-5-content-factory-sdk-regression.md) | P18.5 验证记录:内容工厂 Lime-side SDK regression、Host Bridge SDK client、package-side dirty 边界和下一刀。 | -| [p18-5-3-package-sdk-migration-plan.md](./p18-5-3-package-sdk-migration-plan.md) | P18.5.3 迁移计划:外部 package 写集、SDK facade 迁移步骤、验收和回滚点。 | -| [p18-5-3-owner-handoff.md](./p18-5-3-owner-handoff.md) | P18.5.3 owner handoff 单页:接管前命令、最小写集、禁止项、验证顺序和完成条件。 | -| [p18-6-raw-worker-pre-gate.md](./p18-6-raw-worker-pre-gate.md) | P18.6 验证记录:raw worker 禁止执行边界、受控 workflow DSL、policy 证据和 P19 退出条件。 | -| [p18-completion-audit.md](./p18-completion-audit.md) | P18 completion audit:完成项、阻塞项、验证命令、协作边界和下一刀。 | +| 文档 | 说明 | +| -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| [capability-sdk.md](./capability-sdk.md) | 客户端 Capability SDK、runtime bridge、能力注入、权限拦截和 mock host。 | +| [implementation-plan.md](./implementation-plan.md) | 客户端 App Host 落地实施方案:manifest parser、projection、installer、storage、UI host、worker runtime。 | +| [p0-technical-design.md](./p0-technical-design.md) | P0 技术设计:只读 package、manifest normalize、projection、readiness、cleanup dry-run、Lab 展示。 | +| [p1-mock-capability-host.md](./p1-mock-capability-host.md) | P1 技术设计:SDK facade、MockCapabilityHost、mock artifact/evidence、uninstall delete-data。 | +| [p2-adapter-capability-host.md](./p2-adapter-capability-host.md) | P2 技术设计:AdapterCapabilityHost、本地 adapter store、knowledge / agent adapter、provenance 查询、delete-data 清理。 | +| [p3-ui-extension-host.md](./p3-ui-extension-host.md) | P3 技术设计:受控 UI Host、sandbox policy、injected SDK bridge、Lab 预览。 | +| [p4-content-factory-demo.md](./p4-content-factory-demo.md) | P4 技术设计:内容工厂最小闭环、content table Artifact、Evidence、delete-data 清理。 | +| [p4-workflow-runtime.md](./p4-workflow-runtime.md) | P4.2 技术设计:受控 workflow runtime、白名单 DSL、trace、cancel、policy guard。 | +| [content-factory-app.md](./content-factory-app.md) | 内容工厂 Product-level Agent App 的客户端产品与实现方案。 | +| [v0.3-rebaseline-plan.md](./v0.3-rebaseline-plan.md) | Agent App v0.3 / 内容工厂升级后的 rebaseline 实施计划。 | +| [p5-cloud-bootstrap.md](./p5-cloud-bootstrap.md) | P5 技术计划:Cloud bootstrap 作为客户端 package source 输入,继续保持本地 install / projection / readiness / runtime / cleanup。 | +| [p6-schema-coverage.md](./p6-schema-coverage.md) | P6 技术计划:补齐 v0.3 projection / readiness schema coverage,但不进入正式主路径。 | +| [p7-schema-gate.md](./p7-schema-gate.md) | P7 技术计划:用本地 schema / snapshot gate 防止 projection / readiness 字段漂移。 | +| [p8-setup-resolver.md](./p8-setup-resolver.md) | P8 技术计划:增加 setup resolver 输入和 `needs-setup` readiness 语义。 | +| [p9-setup-state-store.md](./p9-setup-state-store.md) | P9 技术计划:本地 setup state store、readiness 输入和 cleanup preview 集成。 | +| [p10-installed-state-persistence.md](./p10-installed-state-persistence.md) | P10 技术计划:installed state snapshot、in-memory store、cleanup preview 集成。 | +| [p11-local-persistence-adapter.md](./p11-local-persistence-adapter.md) | P11 技术实现:本地 persistence adapter、启动恢复、坏文件隔离、delete-data 清理。 | +| [p12-package-cache-verify-rollback.md](./p12-package-cache-verify-rollback.md) | P12 技术实现:package cache、hash verify、cached fallback、upgrade staging 和 rollback。 | +| [p13-runtime-package-loader.md](./p13-runtime-package-loader.md) | P13 技术实现:runtime package loader、bundle manifest gate、UI host 接线前安全边界。 | +| [p14-entry-runtime-guard-permission-prompt.md](./p14-entry-runtime-guard-permission-prompt.md) | P14 技术计划:entry runtime guard、permission prompt descriptor、setup / readiness / policy 合并。 | +| [p15-lab-install-launch-flow.md](./p15-lab-install-launch-flow.md) | P15 技术实现:Lab install / launch flow、install review、permission prompt、runtime launch、cleanup preview。 | +| [p15-h-gui-smoke-cleanup-rehearsal.md](./p15-h-gui-smoke-cleanup-rehearsal.md) | P15-H 技术实现:Agent App Lab 专用 GUI smoke、证据输出、cleanup rehearsal 与 P16 gate。 | +| [p16-agent-app-manager-product-entry-gate.md](./p16-agent-app-manager-product-entry-gate.md) | P16 技术计划:Agent App Manager、entry launcher、lifecycle actions、cleanup evidence 和正式入口前 gate。 | +| [p16-h-multi-app-repository-lifecycle-hardening.md](./p16-h-multi-app-repository-lifecycle-hardening.md) | P16-H 技术计划:多 App repository list、持久化 lifecycle、cleanup evidence export、residual audit 和 P17 前 gate。 | +| [p17-formal-entry-gate-audit.md](./p17-formal-entry-gate-audit.md) | P17 Gate 审计:正式入口前 checklist、验证记录、进入条件和 P17.0 契约入口。 | +| [p17-formal-entry-contract.md](./p17-formal-entry-contract.md) | P17.0 技术计划:正式 `agent-apps` 入口契约、Lab 边界、runtime surface、架构 / 时序 / 流程、后续 gates。 | +| [p17-source-install-contract-hardening.md](./p17-source-install-contract-hardening.md) | P17.2 技术计划:正式入口 source state、install review、registration、Cloud release metadata 与客户端安装边界。 | +| [p17-lifecycle-cleanup-contract-hardening.md](./p17-lifecycle-cleanup-contract-hardening.md) | P17.3 技术计划:正式入口 lifecycle、cleanup rehearsal、evidence export、residual audit 与 namespace 分离。 | +| [p17-4-host-bridge-runtime.md](./p17-4-host-bridge-runtime.md) | P17.4-H 技术计划:Host Bridge v1、主题同步、Host action、capability invoke、iframe message 安全边界。 | +| [p17-5-formal-entry-gui-smoke.md](./p17-5-formal-entry-gui-smoke.md) | P17.5 验证记录:正式 `agent-apps` 入口独立 GUI smoke、cleanup dry-run evidence 与 flag-off regression。 | +| [p18-typed-capability-sdk-gate.md](./p18-typed-capability-sdk-gate.md) | P18 技术计划:Typed Capability SDK Gate、Host Bridge v1 调用信封、stable error、mock / contract tests、App-scoped Agent task。 | +| [p18-4-h-agentruntime-handoff-gate.md](./p18-4-h-agentruntime-handoff-gate.md) | P18.4-H 验证记录:AgentRuntime handoff checklist、证据映射、剩余 owner 缺口与 P18.5 进入判定。 | +| [p18-5-content-factory-sdk-regression.md](./p18-5-content-factory-sdk-regression.md) | P18.5 验证记录:内容工厂 Lime-side SDK regression、Host Bridge SDK client、package-side dirty 边界和下一刀。 | +| [p18-5-3-package-sdk-migration-plan.md](./p18-5-3-package-sdk-migration-plan.md) | P18.5.3 迁移计划:外部 package 写集、SDK facade 迁移步骤、验收和回滚点。 | +| [p18-5-3-owner-handoff.md](./p18-5-3-owner-handoff.md) | P18.5.3 owner handoff 单页:接管前命令、最小写集、禁止项、验证顺序和完成条件。 | +| [p18-6-raw-worker-pre-gate.md](./p18-6-raw-worker-pre-gate.md) | P18.6 验证记录:raw worker 禁止执行边界、受控 workflow DSL、policy 证据和 P19 退出条件。 | +| [p18-7-full-lime-capability-surface.md](./p18-7-full-lime-capability-surface.md) | P18.7 路线图:全量 Lime capability surface、App / Lime 主 App 边界、v0.6 标准兼容、Host discovery 和后端接线顺序。 | +| [p18-completion-audit.md](./p18-completion-audit.md) | P18 completion audit:完成项、阻塞项、验证命令、协作边界和下一刀。 | ## 客户端边界 @@ -123,6 +125,7 @@ Agent App Runtime Package 10. 通用 Chat 不能成为 Agent App 主流程的强制回跳点;如果核心工作仍需在 Chat 里完成,说明 App 只是 Chat 包装壳,不是 product-level Agent App。 11. App 也不能为了避免回 Chat 而自建模型网关、凭证系统、权限系统、证据系统或工具调度器;这些属于 Lime capability。 12. Agent App SDK 不拥有 Claw / AgentRuntime capability catalog;Chat `@命令`、Agent App `lime.agent.startTask` 和 Automation job 都只是 surface adapter,不能在 App 侧复制 `*_skill_launch` 或新增垂直 `content_factory_*` 后端能力。 +13. 全量 Lime 功能必须先进入 `lime.* capability catalog`,再接 Host / AgentRuntime / ToolRuntime;业务 App 只能消费 capability,不能把 preview 能力 mock 成生产成功。 ## 主路线 @@ -145,7 +148,7 @@ flowchart TD ## 当前执行顺序 -路线图已从 Agent App v0.3 / 内容工厂 rebaseline 推进到上游 v0.4 Host Bridge 对齐;上游标准已补齐 discovery / installation、release metadata、runtime model、security model、overlay resolver、readiness runner、typed Capability SDK、public JSON Schema、reference CLI 与 Host Bridge v1。P4-R、P5、P6、P7、P8、P9、P10、P11、P12、P13、P14、P15、P15-H、P16、P16-H、P17 Gate、P17.0、P17.1、P17.2.1-P17.2.5、P17.3 lifecycle / cleanup contract、P17.4 runtime surface production hardening 与 P17.5 formal entry GUI smoke 已完成当前实现 / 专用验证 / 计划收口。当前进入 P18 Typed Capability SDK Gate,仍不做 marketplace、Cloud 管理台、真实 delete-data 或完整行业内容系统: +路线图已从 Agent App v0.3 / 内容工厂 rebaseline 推进到上游 v0.4 Host Bridge 对齐;当前上游示例已进入 `manifestVersion: 0.6.0` 并新增 `app.runtime.yaml`,P18.7-B 已完成 layered manifest / runtime contract reference cross-check 兼容,下一步继续后端 capability discovery / 接线。上游标准已补齐 discovery / installation、release metadata、runtime model、security model、overlay resolver、readiness runner、typed Capability SDK、public JSON Schema、reference CLI 与 Host Bridge v1。P4-R、P5、P6、P7、P8、P9、P10、P11、P12、P13、P14、P15、P15-H、P16、P16-H、P17 Gate、P17.0、P17.1、P17.2.1-P17.2.5、P17.3 lifecycle / cleanup contract、P17.4 runtime surface production hardening 与 P17.5 formal entry GUI smoke 已完成当前实现 / 专用验证 / 计划收口。当前进入 P18 Typed Capability SDK Gate,仍不做 marketplace、Cloud 管理台、真实 delete-data 或完整行业内容系统: ```text P0 manifest / projection / readiness / cleanup dry-run @@ -205,11 +208,11 @@ P0 manifest / projection / readiness / cleanup dry-run → P18.6 Raw Worker 前 Gate(已完成) ``` -不要先做市场页、Cloud 管理台、完整行业内容系统,也不要让 App 绕过 SDK;P17.5 只证明正式入口独立 smoke 可跑通,不等于发布 marketplace。当前 P18 功能面已收口:Lime-side regression、外部 package tests / validate / readiness、真实 package verify、dist 同步和 handoff gate 均有证据;下一步只做 owner handoff / 提交边界,不再扩大 P18 功能面。 +不要先做市场页、Cloud 管理台、完整行业内容系统,也不要让 App 绕过 SDK;P17.5 只证明正式入口独立 smoke 可跑通,不等于发布 marketplace。当前 P18 功能面已收口:Lime-side regression、外部 package tests / validate / readiness、真实 package verify、dist 同步和 handoff gate 均有证据;下一步只做 P18.7-C Host capability discovery,不扩大 marketplace、Cloud 管理台或垂直内容工厂后端。 ## 下一刀判定 -P5.0-P5.5、P6、P7、P8、P9、P10、P11、P12、P13、P14、P15、P15-H、P16、P16-H、P17 Gate、P17.0、P17.1、P17.2.1-P17.2.5、P17.3、P17.4.1-P17.4.5、P17.5、P18.1、P18.2、P18.3、P18.4、P18.4-H、P18.5.1、P18.5.2、P18.5-S、P18.5.3 与 P18.6 已通过定向 / 专用验证 / GUI smoke / 计划收口;2026-05-16 10:53 SDK seam / handoff core 定向测试 5 files / 17 tests passed,`typecheck`、`test:contracts` 与 `lint` 当前会话复核通过。当前只做 owner handoff: +P5.0-P5.5、P6、P7、P8、P9、P10、P11、P12、P13、P14、P15、P15-H、P16、P16-H、P17 Gate、P17.0、P17.1、P17.2.1-P17.2.5、P17.3、P17.4.1-P17.4.5、P17.5、P18.1、P18.2、P18.3、P18.4、P18.4-H、P18.5.1、P18.5.2、P18.5-S、P18.5.3 与 P18.6 已通过定向 / 专用验证 / GUI smoke / 计划收口;2026-05-16 10:53 SDK seam / handoff core 定向测试 5 files / 17 tests passed,`typecheck`、`test:contracts` 与 `lint` 当前会话复核通过。当前只做 P18.7-C Host capability discovery: 1. P18.5 只做内容工厂现有 package 的 SDK 化回归,不新增 marketplace、Cloud 管理台或完整行业系统。 2. `agent-apps` 是用户入口,`agent-app-lab` 是研发验证入口;P18 不能让 App 回跳通用 Chat 才能完成业务流程。 @@ -219,11 +222,11 @@ P5.0-P5.5、P6、P7、P8、P9、P10、P11、P12、P13、P14、P15、P15-H、P16 协作分工口径: -| 方向 | 负责 | P18 消费方式 | 防打架边界 | -|---|---|---|---| +| 方向 | 负责 | P18 消费方式 | 防打架边界 | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | AgentRuntime current MVP | `agent_app_runtime_*` facade、`AgentRuntimeThreadReadModel`、Host response、`artifact:created` refs、跨刷新恢复、Artifact / Evidence 投影。 | P18.1 已抽象成 SDK 类型和 mock contract;P18.4 已包装为 `lime.agent` typed adapter;P18.4-H 已消费隔壁运行证据。 | 不在 P18.5 改 `src-tauri/*`、`src/lib/api/agentAppRuntime.ts`、`agentRuntimeCapabilityHost*` 实现逻辑。 | -| AgentRuntime 剩余缺口 | 后端 push subscribe、`content_factory.workspace_patch` producer、独立 capability catalog service、真实桌面 GUI smoke。 | 已在 P18.4-H 记录 owner 与退出条件;P18.5 只消费,不伪造生产级完成。 | 不由 Agent App SDK 伪造生产级完成。 | -| Agent App P18 | typed facade、stable error、mock host、capability envelope、SDK contract tests。 | 固定 App 作者只调用 `@lime/app-sdk`,不手写私有 bridge。 | 不扩 marketplace、Cloud 管理台、raw worker、真实 delete-data 或垂直内容工厂后端。 | +| AgentRuntime 剩余缺口 | 后端 push subscribe、`content_factory.workspace_patch` producer、独立 capability catalog service、真实桌面 GUI smoke。 | 已在 P18.4-H 记录 owner 与退出条件;P18.5 只消费,不伪造生产级完成。 | 不由 Agent App SDK 伪造生产级完成。 | +| Agent App P18 | typed facade、stable error、mock host、capability envelope、SDK contract tests。 | 固定 App 作者只调用 `@lime/app-sdk`,不手写私有 bridge。 | 不扩 marketplace、Cloud 管理台、raw worker、真实 delete-data 或垂直内容工厂后端。 | 当前计划更新口径: @@ -248,4 +251,5 @@ P5.0-P5.5、P6、P7、P8、P9、P10、P11、P12、P13、P14、P15、P15-H、P16 - P18.5.1 已完成:新增 Lime-side content factory SDK regression,验证 `lime.agent -> lime.storage -> lime.artifacts -> lime.evidence` 主链能由通用 SDK facade 表达。 - P18.5.2 已完成:外部 `content-factory-app` 只读 `npm test` 在 2026-05-16 09:05 owner handoff gate 复核为 46 tests passed,覆盖 Host Bridge task、stream、Host response、storage、artifact、evidence 写回;该结果不替代 package-side SDK facade / verify。 - P18.6 已完成:新增 raw worker 前 gate,固定 P18 不执行 raw worker、任意外部代码、网络、文件系统或 secret value,相关 runtime policy / guard / workflow tests 已通过。 -- 下一刀 P18.5.3:等待外部 content-factory package owner 稳定后,收敛 package-side 手写 bridge wrapper 并跑 package verify / Lime formal runtime 回归;owner 未确认前不改外部 `src/ui/host-bridge.js`、`tests/ui.test.mjs`、`package.json` 或 `dist/*`。 +- P18.7 已完成第一刀:新增 `src/features/agent-app/sdk/capabilityCatalog.ts`,把 `lime.ui / storage / files / agent / knowledge / tools / artifacts / workflow / policy / secrets / evidence / events / capabilities / models / usage / memory / skills / mcp / browser / search / documents / media / terminal / tasks / settings / workspace / context / connectors / automation / review` 收敛为唯一能力事实源;`capabilityContract`、p0/mock/adapter profile 和 SDK adapter 均从 catalog 派生,preview 能力默认不可用,不在业务 App 内伪造成功。 +- P18.7-B 已完成:Lime 已兼容上游 `manifestVersion: 0.6.0` reference sample、分层 manifest 与 `app.runtime.yaml`,`referenceCliCrossCheck.test.ts` 复绿;下一刀 P18.7-C 只做 Host capability discovery,不改外部 `agentapp` / `content-factory-app`。 diff --git a/docs/roadmap/agentapp/capability-sdk.md b/docs/roadmap/agentapp/capability-sdk.md index 0ee2de982..e2b518d69 100644 --- a/docs/roadmap/agentapp/capability-sdk.md +++ b/docs/roadmap/agentapp/capability-sdk.md @@ -40,22 +40,64 @@ 10. **Agent 不出 Lime 治理**:模型、工具、知识、文件、凭证、Artifact、Evidence、成本和权限都必须通过 `lime.*` capability。 11. **完整 Agent 能力不等于模型 API**:`LIME_GATEWAY_*` / `OPENAI_BASE_URL` 只能是低阶模型 executor 或 degraded fallback;`lime.agent` / `lime.workflow` 的生产事实源必须回到 `docs/roadmap/agentruntime/app-surface-runtime.md` 定义的 AgentRuntime Surface。 -## 能力地图 +## Lime 全功能能力化地图 -| Capability | P0 范围 | 典型 API | -|---|---|---| -| `lime.ui` | 注册 page、panel、command、settings、artifact viewer。 | `registerRoute`、`openPanel`、`openArtifact` | -| `lime.storage` | App namespace、CRUD、schema、migration。 | `namespace`、`table`、`migrate` | -| `lime.files` | 用户选中文件、读取 file ref、基础解析。 | `pick`、`read`、`parse` | -| `lime.agent` | 本地 Agent task、stream、cancel、retry、trace。 | `startTask`、`streamTask`、`cancelTask` | -| `lime.knowledge` | Knowledge binding、search、export、version。 | `bind`、`search`、`export` | -| `lime.tools` | Tool Broker 调用、权限、长任务状态。 | `invoke`、`getProgress` | -| `lime.artifacts` | 创建、读取、打开、导出 Artifact。 | `create`、`open`、`export` | -| `lime.workflow` | workflow state、human review、background task。 | `start`、`checkpoint`、`awaitHuman` | -| `lime.policy` | 权限、风险、成本、企业策略。 | `requestPermission`、`check` | -| `lime.evidence` | provenance、tool call、knowledge citation、eval。 | `record`、`linkArtifact` | -| `lime.secrets` | OAuth、API key、外部凭证槽位。 | `requestSecret`、`getHandle` | -| `lime.events` | App 内外事件,UI/worker 解耦。 | `emit`、`subscribe` | +详细执行路线图见 [P18.7 Full Lime Capability Surface](./p18-7-full-lime-capability-surface.md)。本文保留 SDK / Host Bridge 方案和能力地图摘要;后续代码实施顺序、v0.6 标准兼容和完成审计以 P18.7 文档为准。 + +代码事实源:`src/features/agent-app/sdk/capabilityCatalog.ts`。后续新增、迁移或下线 Lime 能力时,必须先更新该 catalog,再由 `capabilityContract.ts`、`hostCapabilityProfile.ts`、`mockCapabilityProfile.ts`、`adapterCapabilityProfile.ts` 和 SDK public surface 派生;禁止在业务 App、mock、adapter 或文档里再维护第二份能力清单。 + +这张表回答“所有 Lime 功能如何被 App 使用”:App 负责业务形态、状态递进和结果验收;Lime 主 App 负责 AgentRuntime、模型、工具、凭证、策略、证据、成本和平台资源。`current` 代表 SDK 名称和 profile 已进入单一事实源;`preview` 代表先占住统一抽象和边界,底层实现不得在业务 App 内私造。 + +| 分组 | Capability | 阶段 | Lime owner | App 做什么 | Lime 主 App 做什么 | +| ------------- | ------------------- | ------- | ----------------------- | --------------------------------------------- | ---------------------------------------------------------- | +| App surface | `lime.ui` | current | Desktop Host | 决定业务页面、提示和导航触发时机。 | 主题、语言、受控导航、下载、入口校验。 | +| App surface | `lime.events` | preview | Desktop Host | 订阅业务事件,不写私有 bridge。 | 事件路由、namespace 隔离、订阅生命周期。 | +| App surface | `lime.workspace` | preview | Desktop Host | 围绕当前 workspace 展示业务状态。 | workspace 身份、路径 ref、跨平台路径封装。 | +| Data | `lime.storage` | current | Desktop Host | 定义业务对象、schema、写回时机。 | namespace 隔离、持久化、provenance。 | +| Data | `lime.files` | current | Desktop Host | 声明文件类型,把解析结果转为业务草稿。 | 文件授权、file ref、解析器、安全边界。 | +| Data | `lime.knowledge` | current | Knowledge Runtime | 选择知识空间并消费检索结果。 | 知识索引、binding、版本、引用 provenance。 | +| Data | `lime.artifacts` | current | Artifact Runtime | 定义产物类型、内容结构和业务状态联动。 | 产物持久化、viewer/export、来源记录。 | +| Data | `lime.documents` | preview | Tool Runtime | 定义 PDF / Word / Markdown / PPT 的业务落点。 | 文档解析、格式转换、权限和 evidence。 | +| Agent runtime | `lime.agent` | current | AgentRuntime | 组装业务任务输入、期望产物、人工确认。 | session/thread/turn/task、Skills、Tools、模型、Evidence。 | +| Agent runtime | `lime.workflow` | current | AgentRuntime | 定义业务步骤、checkpoint 和人工介入点。 | workflow 状态、恢复、Host response、权限。 | +| Agent runtime | `lime.tools` | current | Tool Runtime | 声明工具需求并消费结构化结果。 | Tool Broker、权限、进度、超时、审计。 | +| Agent runtime | `lime.models` | preview | AgentRuntime | 表达任务偏好、质量/成本约束。 | 模型事实源、Provider 能力、路由和成本估算。 | +| Agent runtime | `lime.memory` | preview | AgentRuntime | 声明可读/可写记忆意图。 | `memory_runtime_*` / `unified_memory_*`、上下文压缩。 | +| Agent runtime | `lime.skills` | preview | AgentRuntime | 声明必需 Skill 和业务场景。 | Skill catalog、workspace binding、runtime gate、调用证据。 | +| Agent runtime | `lime.context` | preview | AgentRuntime | 显式提交当前业务选择和资源上下文。 | session/thread/turn context、压缩和恢复边界。 | +| Agent runtime | `lime.automation` | preview | AgentRuntime | 定义业务触发、输入和终止条件。 | automation job runtime、队列、权限、证据。 | +| Integration | `lime.mcp` | preview | Tool Runtime | 声明需要的 MCP capability。 | MCP bridge、inventory、命名、权限和审计。 | +| Integration | `lime.browser` | preview | Tool Runtime | 表达网页采集目标和用户授权。 | 浏览器 profile、自动化、截图、回放证据。 | +| Integration | `lime.search` | preview | Tool Runtime | 给出业务检索问题和筛选规则。 | 搜索/深搜 provider、来源去重、引用、成本。 | +| Integration | `lime.media` | preview | Tool Runtime | 给出图片/音频/视频 brief 和交付约束。 | 媒体 runtime、文件产物、安全策略、成本。 | +| Integration | `lime.terminal` | preview | Tool Runtime | 说明命令目的和输入。 | sandbox、approval、日志、危险操作拦截。 | +| Integration | `lime.connectors` | preview | Cloud Overlay / Desktop | 声明外部系统连接需求。 | OAuth、secret、tenant policy、连接器审计。 | +| Governance | `lime.policy` | current | Policy Runtime | 说明能力用途并处理拒绝/降级。 | 权限、风险、成本、企业策略和授权。 | +| Governance | `lime.secrets` | current | Policy Runtime | 只保存 secret ref,不读取明文。 | 凭证托管、授权、轮换、最小权限访问。 | +| Governance | `lime.settings` | preview | Desktop Host | 读/改自己的配置域。 | schema、workspace overlay、tenant 默认值、迁移。 | +| Governance | `lime.review` | preview | Policy Runtime | 把审核嵌入业务 UI。 | 审核证据、权限、发布门禁和决策记录。 | +| Governance | `lime.capabilities` | preview | Desktop Host | 根据 profile 做降级,不猜测底层实现。 | 发布能力目录、版本、readiness 和可用性。 | +| Observability | `lime.evidence` | current | Artifact Runtime | 声明证据类型并挂到产物/任务。 | evidence 持久化、导出、审计。 | +| Observability | `lime.usage` | preview | AgentRuntime | 展示业务任务成本并响应预算拦截。 | Token、费用、预算、request telemetry 归因。 | +| Observability | `lime.tasks` | preview | AgentRuntime | 展示本 App 相关后台任务。 | 任务中心、队列、恢复、事件订阅、审计。 | + +### 可用性口径 + +| 层 | current 事实源 | 说明 | +| -------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------ | +| 名称 / 分组 / owner / 阶段 | `capabilityCatalog.ts` | 唯一允许新增 `lime.*` 名称的地方。 | +| TypeScript 调用契约 | `capabilityContract.ts` | 所有 capability 必须有 typed method;未实现也要返回 stable unavailable error。 | +| SDK facade | `capabilityAdapters.ts` | `createLimeCoreCapabilityAdapters()` 按 catalog 自动生成全部 adapter key。 | +| Host readiness profile | `hostCapabilityProfile.ts` | 基础 profile 覆盖所有 capability,默认 `enabled=false / implementation=none`。 | +| Mock / adapter profile | `mockCapabilityProfile.ts`、`adapterCapabilityProfile.ts` | 只从 catalog 标注的能力派生,不再各自维护数组。 | +| 真实执行 | AgentRuntime / ToolRuntime / Desktop Host | 业务 App 不拥有执行事实源;preview 能力未接入时必须显式不可用。 | + +### 边界结论 + +1. 业务 App 可以拥有多个页面、多个工作流、多个业务对象,但不能拥有第二套模型、工具、Skill、凭证、成本、证据或运行过程事实源。 +2. `lime.agent` 只是完整 Agent 能力入口之一;模型选择、Skill、MCP、浏览器、搜索、媒体、终端、记忆、用量和审计都必须继续向对应 `lime.*` capability 收敛。 +3. Claw 已有能力不能复制到 App 内;Chat `@命令`、Agent App task、Automation job 都应成为同一 AgentRuntime / capability catalog 的 surface adapter。 +4. preview capability 不是假入口:profile 默认关闭,调用必须返回 stable unavailable / policy error;只有 Host 真正接线后才可标记 `mock`、`adapter` 或 `native`。 ## 架构图 @@ -93,12 +135,12 @@ Agent App 边界固定如下: -| 层 | 做什么 | 不做什么 | -|---|---|---| -| `@lime/app-sdk` | 暴露 `lime.storage`、`lime.agent`、`lime.workflow` 等稳定 facade。 | 不暴露 Lime internal path,不执行模型和工具。 | -| Host Bridge | 安全传输、主题、语言、capability invoke、Host action。 | 不保存执行事实,不判断任务完成。 | -| Agent App Runtime Surface | 把 App task / workflow 映射成 AgentRuntime request,并附加 app provenance。 | 不复制 Claw skill launch,不新建第二套 runtime。 | -| AgentRuntime | 维护 session/thread/turn/task/event/read model/evidence。 | 不决定垂直 App UI 形态。 | +| 层 | 做什么 | 不做什么 | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| `@lime/app-sdk` | 暴露 `lime.storage`、`lime.agent`、`lime.workflow` 等稳定 facade。 | 不暴露 Lime internal path,不执行模型和工具。 | +| Host Bridge | 安全传输、主题、语言、capability invoke、Host action。 | 不保存执行事实,不判断任务完成。 | +| Agent App Runtime Surface | 把 App task / workflow 映射成 AgentRuntime request,并附加 app provenance。 | 不复制 Claw skill launch,不新建第二套 runtime。 | +| AgentRuntime | 维护 session/thread/turn/task/event/read model/evidence。 | 不决定垂直 App UI 形态。 | | AgentRuntime Capability Catalog / Claw Capability Catalog | 把现有 `@配图`、`@搜索`、`@研报` 等能力注册为可复用 capability;Chat `@命令`、Agent App `lime.agent.startTask`、Automation job 都只是 surface adapter。 | 不再让能力只绑定 Chat/Inputbar,也不把 capability catalog owner 下放给 Agent App SDK。 | 生产期 `lime.agent.startTask` / `lime.workflow.start` 必须通过后端 AgentRuntime Surface;前端 `CapabilityHost` / `WorkflowRuntimeHost` 只能作为 adapter、mock 或本地预览,不得成为生产执行事实源。详细设计见: @@ -136,15 +178,15 @@ interface LimeAgentAppBridgeMessage { 首版 Host Bridge 覆盖: -| 方向 | 事件 | 说明 | -|---|---|---| -| Host -> App | `host:snapshot` / `theme:update` | 同步主题、语言、入口上下文、capability 摘要。 | -| Host -> App | `host:response` / `host:error` | 按 `requestId` 返回 SDK / Host action 结果。 | -| Host -> App | `host:visibility` | 页面可见性变化,供 App 暂停或恢复轻量同步。 | -| App -> Host | `app:ready` / `host:getSnapshot` | App 初始化和快照补偿。 | -| App -> Host | `host:toast` / `host:navigate` | 非技术提示和受控导航。 | -| App -> Host | `host:openExternal` / `host:download` | 受控外链和同源产物下载。 | -| App -> Host | `capability:invoke` | SDK capability 调用统一入口。 | +| 方向 | 事件 | 说明 | +| ----------- | ------------------------------------- | --------------------------------------------- | +| Host -> App | `host:snapshot` / `theme:update` | 同步主题、语言、入口上下文、capability 摘要。 | +| Host -> App | `host:response` / `host:error` | 按 `requestId` 返回 SDK / Host action 结果。 | +| Host -> App | `host:visibility` | 页面可见性变化,供 App 暂停或恢复轻量同步。 | +| App -> Host | `app:ready` / `host:getSnapshot` | App 初始化和快照补偿。 | +| App -> Host | `host:toast` / `host:navigate` | 非技术提示和受控导航。 | +| App -> Host | `host:openExternal` / `host:download` | 受控外链和同源产物下载。 | +| App -> Host | `capability:invoke` | SDK capability 调用统一入口。 | 安全规则: @@ -196,14 +238,14 @@ await lime.agent.startTask({ 客户端当前实现位置: -| 对象 | 作用 | -|---|---| -| `src/features/agent-app/sdk/hostBridgeClient.ts` | 把 typed SDK request 转成 Host Bridge v1 `capability:invoke` message,处理 `app:ready`、`host:getSnapshot`、`host:toast`、`host:navigate`、`host:openExternal`、`host:download`、`host:snapshot`、`theme:update`、`host:visibility`、`host:response` / `host:error`、trusted origin、timeout、pending cleanup,以及 `capability:subscribe / unsubscribe / event`。 | -| `src/features/agent-app/sdk/index.ts` | SDK-only public surface,只导出 capability facade、stable error、Host Bridge client、mock host 和 App package 需要的 task / storage / artifact / evidence 类型;不导出 UI、installer、repository 或 runtime host 内部实现。 | -| `src/features/agent-app/index.ts` | 当前 Lime repo public feature entry 已导出 `createLimeCoreCapabilityAdapters`、`createLimeHostBridgeCapabilityInvoker`、`LIME_AGENT_APP_BRIDGE_PROTOCOL` 与 `LIME_AGENT_APP_BRIDGE_VERSION`,作为后续正式 SDK package / package-local shim 的事实源。 | -| `src/features/agent-app/sdk/hostBridgeClient.test.ts` | 覆盖 ready / snapshot / theme / visibility / toast / navigate / openExternal / download、envelope、stable error、timeout cleanup、capability subscription 事件分发,以及内容工厂 task / storage / artifact / evidence 主链。 | -| `src/features/agent-app/sdk/publicSdkSurface.test.ts` | 固定 SDK-only public surface,确保后续正式 `@lime/app-sdk` 或 package-local shim 可以从窄导出清单取能力;同时从运行时 namespace 和源码 export 来源两侧禁止导出 UI、安装器、repository、runtime host、adapter、schema 或 dispatcher 内部对象。 | -| `src/features/agent-app/index.test.ts` | 固定 public feature entry 的 SDK export seam,防止后续 App 只能依赖 Lime 内部深路径。 | +| 对象 | 作用 | +| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `src/features/agent-app/sdk/hostBridgeClient.ts` | 把 typed SDK request 转成 Host Bridge v1 `capability:invoke` message,处理 `app:ready`、`host:getSnapshot`、`host:toast`、`host:navigate`、`host:openExternal`、`host:download`、`host:snapshot`、`theme:update`、`host:visibility`、`host:response` / `host:error`、trusted origin、timeout、pending cleanup,以及 `capability:subscribe / unsubscribe / event`。 | +| `src/features/agent-app/sdk/index.ts` | SDK-only public surface,只导出 capability facade、stable error、Host Bridge client、mock host 和 App package 需要的 task / storage / artifact / evidence 类型;不导出 UI、installer、repository 或 runtime host 内部实现。 | +| `src/features/agent-app/index.ts` | 当前 Lime repo public feature entry 已导出 `createLimeCoreCapabilityAdapters`、`createLimeHostBridgeCapabilityInvoker`、`LIME_AGENT_APP_BRIDGE_PROTOCOL` 与 `LIME_AGENT_APP_BRIDGE_VERSION`,作为后续正式 SDK package / package-local shim 的事实源。 | +| `src/features/agent-app/sdk/hostBridgeClient.test.ts` | 覆盖 ready / snapshot / theme / visibility / toast / navigate / openExternal / download、envelope、stable error、timeout cleanup、capability subscription 事件分发,以及内容工厂 task / storage / artifact / evidence 主链。 | +| `src/features/agent-app/sdk/publicSdkSurface.test.ts` | 固定 SDK-only public surface,确保后续正式 `@lime/app-sdk` 或 package-local shim 可以从窄导出清单取能力;同时从运行时 namespace 和源码 export 来源两侧禁止导出 UI、安装器、repository、runtime host、adapter、schema 或 dispatcher 内部对象。 | +| `src/features/agent-app/index.test.ts` | 固定 public feature entry 的 SDK export seam,防止后续 App 只能依赖 Lime 内部深路径。 | 发布状态与退出条件:正式 `@lime/app-sdk` package 尚未从 Lime repo 独立发布;外部 App 仍不得 import `src/features/agent-app/*` 或其它 Lime internal path。P18.5.3 如果需要 package-local shim,只能镜像本节 public API,并在正式 SDK package 可安装后退出。 @@ -245,12 +287,12 @@ interface LimeAgentTaskRequest { 最小运行语义: -| 阶段 | App 责任 | Lime Host / Agent 责任 | -|---|---|---| -| Start | 从当前页面 / workflow 组装业务输入、期望结构和幂等键。 | 校验 manifest、entry readiness、permission、policy、cost。 | -| Stream | 在 App 内显示进度、引用、工具调用、错误和可取消状态。 | 发送 `taskId`、`traceId`、status、tool call、citation、partial artifact、blocked error。 | -| Review | 让用户编辑、确认、拒绝或重试结果。 | 保留 trace,确保重试和取消可审计。 | -| Write-back | 通过 `lime.storage` 写业务对象,通过 `lime.artifacts` / `lime.evidence` 写交付物和依据。 | 自动附加 appId、entryKey、package provenance、workspace / tenant 上下文。 | +| 阶段 | App 责任 | Lime Host / Agent 责任 | +| ---------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Start | 从当前页面 / workflow 组装业务输入、期望结构和幂等键。 | 校验 manifest、entry readiness、permission、policy、cost。 | +| Stream | 在 App 内显示进度、引用、工具调用、错误和可取消状态。 | 发送 `taskId`、`traceId`、status、tool call、citation、partial artifact、blocked error。 | +| Review | 让用户编辑、确认、拒绝或重试结果。 | 保留 trace,确保重试和取消可审计。 | +| Write-back | 通过 `lime.storage` 写业务对象,通过 `lime.artifacts` / `lime.evidence` 写交付物和依据。 | 自动附加 appId、entryKey、package provenance、workspace / tenant 上下文。 | 验收口径:内容工厂的资料整理、场景生成、批量文案、交付和复盘都应在 App 页面内启动、观察、确认和写回;Expert Chat 只能作为嵌入式协作者读取同一上下文,不允许成为手工复制结果的旁路。 @@ -335,15 +377,15 @@ entries: ## P0 交付物 -| 交付物 | 说明 | 验收 | -|---|---|---| +| 交付物 | 说明 | 验收 | +| ------------------------ | --------------------------------------------------------- | -------------------------------- | | App manifest v0.3 parser | 支持 `requires`、`runtimePackage`、`storage`、`entries`。 | 示例 App 可 validate / project。 | -| Capability SDK 类型草案 | TypeScript types + mock host。 | App 示例可以用 mock 运行单测。 | -| Desktop Installer 方案 | 安装、hash、projection、readiness、权限。 | 能生成 projection,不运行代码。 | -| Storage namespace 方案 | schema、migration、保留/删除策略。 | 卸载时可选择保留数据。 | -| UI extension slot 方案 | page / panel / settings / artifact viewer。 | App 页面不需要写进 Core。 | -| Worker runtime 方案 | long task、cancel、trace、policy。 | 能执行受控后台任务。 | -| Evidence 串联 | task/tool/knowledge/artifact/eval provenance。 | 产物能追溯 App 和知识版本。 | +| Capability SDK 类型草案 | TypeScript types + mock host。 | App 示例可以用 mock 运行单测。 | +| Desktop Installer 方案 | 安装、hash、projection、readiness、权限。 | 能生成 projection,不运行代码。 | +| Storage namespace 方案 | schema、migration、保留/删除策略。 | 卸载时可选择保留数据。 | +| UI extension slot 方案 | page / panel / settings / artifact viewer。 | App 页面不需要写进 Core。 | +| Worker runtime 方案 | long task、cancel、trace、policy。 | 能执行受控后台任务。 | +| Evidence 串联 | task/tool/knowledge/artifact/eval provenance。 | 产物能追溯 App 和知识版本。 | ## 分期计划 @@ -377,11 +419,11 @@ entries: ## 风险与应对 -| 风险 | 影响 | 应对 | -|---|---|---| -| SDK 过厚 | 变成第二套 Lime 内部 API。 | 只暴露 capability facade,不暴露 store/internal path。 | -| SDK 过薄 | App 重复造轮子。 | P0 优先封装高频底座:storage、files、agent、artifact、knowledge、tools。 | -| UI 安全 | App UI 诱导授权或越权访问。 | Host 控制容器 + runtime permission bridge 双拦截。 | -| Migration 破坏数据 | App 升级损坏用户数据。 | migration plan、dry-run、backup、保留数据策略。 | -| Cloud 变 Runtime | 破坏 Lime 本地运行定位。 | server-assisted 必须显式声明并受 policy 控制。 | +| 风险 | 影响 | 应对 | +| -------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------- | +| SDK 过厚 | 变成第二套 Lime 内部 API。 | 只暴露 capability facade,不暴露 store/internal path。 | +| SDK 过薄 | App 重复造轮子。 | P0 优先封装高频底座:storage、files、agent、artifact、knowledge、tools。 | +| UI 安全 | App UI 诱导授权或越权访问。 | Host 控制容器 + runtime permission bridge 双拦截。 | +| Migration 破坏数据 | App 升级损坏用户数据。 | migration plan、dry-run、backup、保留数据策略。 | +| Cloud 变 Runtime | 破坏 Lime 本地运行定位。 | server-assisted 必须显式声明并受 policy 控制。 | | Agent 能力退化为 API | App 绕过 Claw / Aster / Evidence 主链。 | `lime.agent` / `lime.workflow` 必须进入 AgentRuntime Surface,模型 token 仅作 executor / fallback。 | diff --git a/docs/roadmap/agentapp/content-factory-app.md b/docs/roadmap/agentapp/content-factory-app.md index 82e7adae5..aa9c266fe 100644 --- a/docs/roadmap/agentapp/content-factory-app.md +++ b/docs/roadmap/agentapp/content-factory-app.md @@ -29,7 +29,9 @@ 后端事实源:内容工厂的 AI 任务必须通过 `docs/roadmap/agentruntime/app-surface-runtime.md` 定义的 Agent App Runtime Surface 进入 AgentRuntime 主链。`LIME_GATEWAY_*` / 模型 API 只能作为低阶 executor 或降级能力,不能替代 Lime Agent 的工具、知识、权限、证据和 artifact 治理。 -当前实现进展:Lime Host 的 `AgentAppRuntimePage` 已把 iframe Host Bridge 里的 `lime.agent.startTask / streamTask / getTask / cancelTask / retryTask / submitHostResponse` 接入 `agent_app_runtime_*` facade,并由后端复用 `AgentRuntimeThreadReadModel` 投影 `taskStatus / taskEvents`。这意味着内容工厂页面内发起“生成文案 / 场景规划”等 Agent task 时,第一跳已经进入完整 Lime AgentRuntime 主链;当 runtime 追问上下文、等待确认或要求工具授权时,App 也能在页面内回传响应,而不是跳回通用 Chat。当前仍未完成实时 stream、artifact-created 专用投影、storage / artifact / evidence 的后端持久写回与跨刷新 task/session 恢复。 +当前实现进展:Lime Host 的 `AgentAppRuntimePage` 已把 iframe Host Bridge 里的 `lime.agent.startTask / streamTask / getTask / cancelTask / retryTask / submitHostResponse` 接入 `agent_app_runtime_*` facade,并由后端复用 `AgentRuntimeThreadReadModel` 投影 `taskStatus / taskEvents`。这意味着内容工厂页面内发起“生成文案 / 场景规划”等 Agent task 时,第一跳已经进入完整 Lime AgentRuntime 主链;当 runtime 追问上下文、等待确认或要求工具授权时,App 也能在页面内回传响应,而不是跳回通用 Chat。Host Bridge / `AgentRuntimeCapabilityHost` 负责把 Claw 式过程进一步封装为标准 `runtimeProcess` / `process`:包含思考、正文流、执行流、Skill、工具、模型、Token、费用、产物、证据、终态与默认折叠策略。内容工厂只消费该视图做业务展示,不再维护自己的底层运行过程解析器。当前剩余缺口是更多真实多 capability 编排、后端/cross-surface capability policy owner,以及重启宿主后的端到端业务流覆盖。 + +新增硬验收:内容工厂不是“本地 Web + 生成 API”。知识库整理、场景地图、内容战役、脚本 / 图片提示词、交付包和复盘只要触发 AI 业务动作,就必须构造 `ContentFactoryAgentTask` 并通过 `lime.agent` 进入 AgentRuntime。页面必须展示 AI 运行现场:思考、执行、流式输出、Skill、工具、模型、Token、费用、artifact、evidence;完成后可以折叠,但过程不能消失。Host 已连接时,`/api/scene/generate`、`/api/copy/batch`、`/api/script/generate`、`/api/strategy/analyze`、`/api/review/submit` 等本地生成接口只能作为开发 fallback,不能承接正式生成。 ## 背景 @@ -50,13 +52,13 @@ ## 用户与收益 -| 用户 | 核心诉求 | 收益 | -|------|----------|------| -| 内容运营团队 | 项目资料一次沉淀,批量生产文案、脚本、提示词。 | 从灵感驱动变为系统化生产。 | -| 个人 IP / 讲师 | 基于本人经历和表达风格产出文章、课程稿、朋友圈和私域话术。 | 语气一致、素材复用、减少重复讲背景。 | -| 交付顾问 | 为客户交付可运行系统,而不是复制 prompt 或 fork Lime。 | 交付可复制,官方 App 可升级,客户数据不被覆盖。 | -| 团队管理员 | 控制 App 可见性、模型、工具、默认知识模板、质量门禁。 | 团队能力标准化、权限可控。 | -| Lime 平台 | 验证 App SDK、UI extension、storage namespace、worker runtime、artifact/evidence。 | 新行业通过 App 扩展,不污染 Core。 | +| 用户 | 核心诉求 | 收益 | +| -------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------- | +| 内容运营团队 | 项目资料一次沉淀,批量生产文案、脚本、提示词。 | 从灵感驱动变为系统化生产。 | +| 个人 IP / 讲师 | 基于本人经历和表达风格产出文章、课程稿、朋友圈和私域话术。 | 语气一致、素材复用、减少重复讲背景。 | +| 交付顾问 | 为客户交付可运行系统,而不是复制 prompt 或 fork Lime。 | 交付可复制,官方 App 可升级,客户数据不被覆盖。 | +| 团队管理员 | 控制 App 可见性、模型、工具、默认知识模板、质量门禁。 | 团队能力标准化、权限可控。 | +| Lime 平台 | 验证 App SDK、UI extension、storage namespace、worker runtime、artifact/evidence。 | 新行业通过 App 扩展,不污染 Core。 | ## 非目标 @@ -72,34 +74,31 @@ ```text 内容工厂 -├── 项目首页 -├── 知识库 -│ ├── IP 知识库 -│ ├── 项目知识库 -│ └── 素材库 -├── 内容场景规划 -├── 内容工厂 -│ ├── 批量文案 -│ ├── 短视频脚本 -│ └── 图片提示词 -├── 策略分析 -├── PPT / 报告 -├── 数据复盘 -├── 交付物 -├── 专家对话 -└── 设置 +├── 项目组合 / 项目驾驶舱 +├── 知识库底座 +├── 场景地图 +├── 内容战役 +│ ├── 战役设置 +│ ├── 场景包 +│ ├── 文案批次 +│ ├── 脚本与图片提示词 +│ ├── 质量检查 +│ └── 提交确认 +├── 交付包 +├── 复盘与下一轮 +└── 项目设置 ``` ### Expert entries 专家只是 App 内的入口: -| Expert | 用途 | -|---|---| -| 内容策略专家 | 项目定位、竞品分析、内容方向判断。 | -| 知识库整理专家 | 协助资料分层、裁剪、版本化。 | -| 文案优化专家 | 去 AI 味、平台适配、人工改稿。 | -| 数据复盘专家 | 解释方向表现、生成下周建议。 | +| Expert | 用途 | +| -------------- | ---------------------------------- | +| 内容策略专家 | 项目定位、竞品分析、内容方向判断。 | +| 知识库整理专家 | 协助资料分层、裁剪、版本化。 | +| 文案优化专家 | 去 AI 味、平台适配、人工改稿。 | +| 数据复盘专家 | 解释方向表现、生成下周建议。 | ## App Package 草案 @@ -204,17 +203,17 @@ knowledgeTemplates: ## Storage 模型 -| 表 | 说明 | -|---|---| -| `projects` | 项目、行业、平台、是否需要 IP、默认模型和状态。 | -| `knowledge_spaces` | IP / project / material 三层知识库空间、版本和健康度。 | -| `knowledge_items` | 分区知识条目、来源、证据、字数、版本。 | +| 表 | 说明 | +| ------------------- | -------------------------------------------------------- | +| `projects` | 项目、行业、平台、是否需要 IP、默认模型和状态。 | +| `knowledge_spaces` | IP / project / material 三层知识库空间、版本和健康度。 | +| `knowledge_items` | 分区知识条目、来源、证据、字数、版本。 | | `content_scenarios` | 内容场景规划结果:维度、痛点、解决方案、决策阶段、标签。 | -| `content_assets` | 文案、脚本、图片提示词、报告等内容资产。 | -| `review_imports` | CSV / Excel 数据导入批次。 | -| `metrics` | 完播率、搜索占比、转化、素材复用率等指标。 | -| `insights` | 自动归因、方向判断、下步建议。 | -| `app_settings` | 禁用词、平台规则、评分阈值、默认导出格式。 | +| `content_assets` | 文案、脚本、图片提示词、报告等内容资产。 | +| `review_imports` | CSV / Excel 数据导入批次。 | +| `metrics` | 完播率、搜索占比、转化、素材复用率等指标。 | +| `insights` | 自动归因、方向判断、下步建议。 | +| `app_settings` | 禁用词、平台规则、评分阈值、默认导出格式。 | ## Workflow @@ -255,28 +254,35 @@ flowchart TD ## Capability 调用映射 -| 业务功能 | Lime Capability | -|---|---| -| 文件上传和解析 | `lime.files`、`lime.tools` | -| 三层知识库管理 | `lime.storage`、`lime.knowledge` | -| 结构化抽取 / 文案 / 脚本 | `lime.agent`、Agent Skills | -| 批量任务、进度、中断 | `lime.workflow`、`lime.agent` | -| 内容表、报告、PPT | `lime.artifacts` | -| 事实来源、版本、评估 | `lime.evidence`、Evals | -| 外部调研、飞书同步 | `lime.tools`、`lime.secrets` | -| 权限、成本、风险 | `lime.policy` | +全量 Lime 能力抽象、App / Lime 主 App 边界和后端接线顺序见 [P18.7 Full Lime Capability Surface](./p18-7-full-lime-capability-surface.md)。内容工厂只作为第一个业务验证 App,不拥有底层 AgentRuntime / ToolRuntime 能力事实源。 + +内容工厂只实现内容业务层,不实现 Lime 底层 AI 平台层。所有 Lime 功能先看 `docs/roadmap/agentapp/capability-sdk.md` 的全量能力地图;内容工厂 MVP 消费其中一部分,后续扩展也必须继续走 `lime.*` capability,而不是新增 `content_factory_*` 专用后端命令或裸模型 API。 + +| 业务功能 | 主要 Capability | 说明 | +| --------------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| 文件上传和解析 | `lime.files`、`lime.documents`、`lime.tools` | App 选择资料类型和业务落点;Lime 负责 file ref、解析器、工具执行和证据。 | +| 三层知识库管理 | `lime.storage`、`lime.knowledge`、`lime.evidence` | App 管 IP / 项目 / 素材三层业务模型;Lime 管 namespace、检索、binding、版本和来源。 | +| 结构化抽取 / 场景 / 文案 / 脚本 | `lime.agent`、`lime.skills`、`lime.models`、`lime.memory` | App 组织 task input 和 expected output;AgentRuntime 负责模型、Skill、上下文、记忆和运行事实。 | +| 批量任务、进度、中断 | `lime.workflow`、`lime.tasks`、`lime.events` | App 展示阶段和人工确认;Lime 托管 checkpoint、任务中心、事件订阅和恢复。 | +| 运行过程、思考、工具、模型、Token、费用 | `lime.agent`、`lime.usage`、`lime.capabilities` | App 只消费 Host 下发的 `runtimeProcess`;模型、用量和可用性来自 Lime。 | +| 内容表、报告、PPT、交付物 | `lime.artifacts`、`lime.documents`、`lime.media` | App 定义交付结构;Lime 负责 artifact、导出、图片/音频/视频/文档 runtime。 | +| 外部调研和网页采集 | `lime.search`、`lime.browser`、`lime.mcp`、`lime.connectors` | App 给出调研问题和筛选规则;Lime 负责搜索、浏览器、MCP、连接器和审计。 | +| 凭证、权限、成本、风险、审核 | `lime.secrets`、`lime.policy`、`lime.settings`、`lime.review` | App 解释用途并处理降级;Lime 托管 secret、策略、设置 overlay 和发布/风险审核。 | +| 产物证据和复盘 | `lime.evidence`、`lime.usage`、`lime.tasks` | App 展示来源和复盘指标;Lime 从 runtime facts / telemetry 归因。 | + +边界结论:内容工厂可以有自己的多页面、多工作流和业务数据库视图,但模型路由、Skill 调用、MCP、浏览器、搜索、媒体、终端、凭证、Token 费用、Evidence Pack 和运行过程都必须由 Lime 主 App capability 提供。 ## App 内 Agent 任务闭环 内容工厂的 Agent 能力必须作为业务任务嵌入 App,而不是跳转到通用 Chat: -| 业务任务 | App 输入 | Lime Agent / Capability | App 内写回 | -|---|---|---|---| -| 资料整理 | 用户授权文件、资料类型、项目目标。 | `lime.files` / `lime.tools` 解析,`lime.agent.startTask` 结构化抽取,`lime.evidence` 记录来源。 | `knowledge_items` 草稿、资料健康度、人工确认状态。 | -| 场景规划 | 已确认知识库、目标平台、人群和数量。 | `lime.knowledge.search` 检索,`lime.agent.startTask` 生成结构化场景。 | `content_scenarios` 表、场景 Artifact、引用证据。 | -| 批量文案 / 脚本 | 场景、平台规则、数量、风格约束。 | `lime.agent.startTask` + writer skills,Eval 检查事实支撑和去 AI 味。 | `content_assets` / `script_batches`、A/B/C 分级、质量记录。 | -| 策略交付 | 内容批次、项目事实、竞品资料。 | `lime.agent.startTask` 生成报告结构,`lime.artifacts` 持久化交付物。 | 策略报告、PPT 大纲、交付状态。 | -| 数据复盘 | CSV / Excel 指标、历史内容资产。 | `lime.tools` 归一化,`lime.agent.startTask` 归因分析。 | `review_reports`、下轮动作、选题权重更新。 | +| 业务任务 | App 输入 | Lime Agent / Capability | App 内写回 | +| --------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| 资料整理 | 用户授权文件、资料类型、项目目标。 | `lime.files` / `lime.tools` 解析,`lime.agent.startTask` 结构化抽取,`lime.evidence` 记录来源。 | `knowledge_items` 草稿、资料健康度、人工确认状态。 | +| 场景规划 | 已确认知识库、目标平台、人群和数量。 | `lime.knowledge.search` 检索,`lime.agent.startTask` 生成结构化场景。 | `content_scenarios` 表、场景 Artifact、引用证据。 | +| 批量文案 / 脚本 | 场景、平台规则、数量、风格约束。 | `lime.agent.startTask` + writer skills,Eval 检查事实支撑和去 AI 味。 | `content_assets` / `script_batches`、A/B/C 分级、质量记录。 | +| 策略交付 | 内容批次、项目事实、竞品资料。 | `lime.agent.startTask` 生成报告结构,`lime.artifacts` 持久化交付物。 | 策略报告、PPT 大纲、交付状态。 | +| 数据复盘 | CSV / Excel 指标、历史内容资产。 | `lime.tools` 归一化,`lime.agent.startTask` 归因分析。 | `review_reports`、下轮动作、选题权重更新。 | 每个任务都必须在内容工厂页面内展示进度、引用、工具调用、失败原因、重试和人工确认;最终结果以结构化对象写回 App storage,再生成 Artifact / Evidence。 @@ -299,33 +305,33 @@ flowchart TD 最小 task 输入: -| 字段 | 来源 | -|---|---| -| `taskKind` | `content_factory.copy.generate` 或 `content_factory.copy.rewrite` | -| `task` | “今天要完成什么”输入框 | -| `category` | 品类输入框 | -| `platform` | 平台选择 | -| `audience` | 目标人群 | -| `coreWords` | 核心词 | -| `materialVersion` | 生产前检查中的资料版本 | -| `selectedScenarios` | 优先场景或自动补齐场景 | -| `scriptRefs` | 现有脚本 / 视频口播引用 | -| `expectedOutput` | 文案、素材 brief、脚本片段、交付包 | -| `humanReview` | 默认 `true` | +| 字段 | 来源 | +| ------------------- | ----------------------------------------------------------------- | +| `taskKind` | `content_factory.copy.generate` 或 `content_factory.copy.rewrite` | +| `task` | “今天要完成什么”输入框 | +| `category` | 品类输入框 | +| `platform` | 平台选择 | +| `audience` | 目标人群 | +| `coreWords` | 核心词 | +| `materialVersion` | 生产前检查中的资料版本 | +| `selectedScenarios` | 优先场景或自动补齐场景 | +| `scriptRefs` | 现有脚本 / 视频口播引用 | +| `expectedOutput` | 文案、素材 brief、脚本片段、交付包 | +| `humanReview` | 默认 `true` | 最小事件投影: -| 事件 | 页面反馈 | -|---|---| -| `task:started` | 本轮任务开始,锁定输入快照。 | -| `task:contextChecked` | 更新“生产前检查”卡片,不再只显示静态数字。 | -| `task:missingContextRequested` | 在 App 内补资料、补场景或追问用户。 | -| `task:toolCall` | 显示知识检索、搜索、图片、报告、PDF / 总结等工具进度。 | -| `task:citation` | 显示资料版本、场景、网页或文件引用。 | -| `task:partialArtifact` | 展示草稿文案、素材 brief、脚本片段。 | -| `task:reviewRequested` | 用户在当前页面确认、编辑、拒绝或重试。 | -| `task:completed` | 写回内容资产并生成交付物。 | -| `artifact:created` / `evidence:recorded` | 交付物和证据已落库。 | +| 事件 | 页面反馈 | +| ---------------------------------------- | ------------------------------------------------------ | +| `task:started` | 本轮任务开始,锁定输入快照。 | +| `task:contextChecked` | 更新“生产前检查”卡片,不再只显示静态数字。 | +| `task:missingContextRequested` | 在 App 内补资料、补场景或追问用户。 | +| `task:toolCall` | 显示知识检索、搜索、图片、报告、PDF / 总结等工具进度。 | +| `task:citation` | 显示资料版本、场景、网页或文件引用。 | +| `task:partialArtifact` | 展示草稿文案、素材 brief、脚本片段。 | +| `task:reviewRequested` | 用户在当前页面确认、编辑、拒绝或重试。 | +| `task:completed` | 写回内容资产并生成交付物。 | +| `artifact:created` / `evidence:recorded` | 交付物和证据已落库。 | “只重写文案”必须限制 artifact policy:只允许改写当前文案资产,不允许重写资料、场景或脚本来源。“生成文案和配套素材”可以触发资料检查、场景补齐、素材 brief 和配图能力,但所有副作用都必须带 App provenance。 @@ -333,25 +339,25 @@ flowchart TD 内容工厂不得复制 Claw 已有能力,首批复用关系见 `docs/roadmap/agentruntime/claw-capability-sharing.md`: -| 内容工厂需求 | 复用 capability | 后端主链 | -|---|---|---| -| 生产前资料补齐 | `lime.capability.research.search` / `lime.capability.summary.generate` | `research_skill_launch` / `summary_skill_launch` | -| 竞品和策略分析 | `lime.capability.report.generate` | `report_skill_launch` | -| PDF / 资料读取 | `lime.capability.pdf.read` | `pdf_read_skill_launch` | -| 配套素材 / 海报 brief | `lime.capability.image.generate` / `lime.capability.cover.generate` | `image_skill_launch` / `cover_skill_launch` | -| 交付 PPT / 报告 | `lime.capability.presentation.generate` / `lime.capability.webpage.generate` | `presentation_skill_launch` / `webpage_skill_launch` | +| 内容工厂需求 | 复用 capability | 后端主链 | +| --------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------- | +| 生产前资料补齐 | `lime.capability.research.search` / `lime.capability.summary.generate` | `research_skill_launch` / `summary_skill_launch` | +| 竞品和策略分析 | `lime.capability.report.generate` | `report_skill_launch` | +| PDF / 资料读取 | `lime.capability.pdf.read` | `pdf_read_skill_launch` | +| 配套素材 / 海报 brief | `lime.capability.image.generate` / `lime.capability.cover.generate` | `image_skill_launch` / `cover_skill_launch` | +| 交付 PPT / 报告 | `lime.capability.presentation.generate` / `lime.capability.webpage.generate` | `presentation_skill_launch` / `webpage_skill_launch` | ## Host Bridge 要求 内容工厂是第一个正式样板 App,不能使用私有 iframe 协议。它的 UI runtime 必须接入 Agent App 标准 Host Bridge: -| 场景 | Bridge 事件 | 验收 | -|---|---|---| -| 跟随 Lime 主题 / 配色 | `host:snapshot`、`theme:update` | 主 App 切换主题后,内容工厂 iframe 内字体、颜色、背景和选中态同步变化。 | -| 非技术提示 | `host:toast` | App 内操作结果用 Lime Host 提示,不暴露 Gateway、Artifact、RAG 等技术词。 | -| 页面跳转 | `host:navigate` | 从项目中心进入资料、写文案、做脚本等入口时仍由 Host 校验 entry / route。 | -| 下载交付物 | `host:download` | 只允许下载同源 runtime 产物或 Host 已授权 Artifact。 | -| 后续能力调用 | `capability:invoke` | 资料解析、模型生成、Artifact 写入继续经过 readiness / permission / policy,不返回 mock 成功。 | +| 场景 | Bridge 事件 | 验收 | +| --------------------- | ------------------------------- | --------------------------------------------------------------------------------------------- | +| 跟随 Lime 主题 / 配色 | `host:snapshot`、`theme:update` | 主 App 切换主题后,内容工厂 iframe 内字体、颜色、背景和选中态同步变化。 | +| 非技术提示 | `host:toast` | App 内操作结果用 Lime Host 提示,不暴露 Gateway、Artifact、RAG 等技术词。 | +| 页面跳转 | `host:navigate` | 从项目中心进入资料、写文案、做脚本等入口时仍由 Host 校验 entry / route。 | +| 下载交付物 | `host:download` | 只允许下载同源 runtime 产物或 Host 已授权 Artifact。 | +| 后续能力调用 | `capability:invoke` | 资料解析、模型生成、Artifact 写入继续经过 readiness / permission / policy,不返回 mock 成功。 | 内容工厂 App 内部只保留主题 fallback,方便脱离 Lime Host 做本地预览;真实嵌入 Lime 时,主题、语言、入口上下文和 Host actions 都以 `lime.agentApp.bridge` 快照为准。 @@ -389,25 +395,25 @@ P0 不做: ## 分期计划 -| 阶段 | 目标 | 验收 | -|---|---|---| +| 阶段 | 目标 | 验收 | +| ---------------- | --------------------------------------------------------- | ------------------------------------------------------ | | P0 App Host 验证 | 用 mock SDK 跑通 App UI + storage + workflow + artifact。 | 不修改 Lime Core 业务代码即可打开 App 页面和保存数据。 | -| P1 内容生产 MVP | 跑通知识库构建、内容场景规划、批量文案 / 脚本。 | 30 秒内输出 20 条,评分和分级可见,Artifact 可追溯。 | -| P2 策略交付 | 策略分析、报告、PPT 大纲或 pptx 导出。 | 输入项目与竞品资料,10 分钟内生成可编辑交付物。 | -| P3 数据飞轮 | 运营数据导入、自动归因、选题库迭代。 | 导入一周数据后自动生成复盘 Artifact。 | -| P4 企业启用 | Cloud catalog、tenant enablement、overlay、secrets。 | 租户可启用 App,客户数据不进入官方包。 | +| P1 内容生产 MVP | 跑通知识库构建、内容场景规划、批量文案 / 脚本。 | 30 秒内输出 20 条,评分和分级可见,Artifact 可追溯。 | +| P2 策略交付 | 策略分析、报告、PPT 大纲或 pptx 导出。 | 输入项目与竞品资料,10 分钟内生成可编辑交付物。 | +| P3 数据飞轮 | 运营数据导入、自动归因、选题库迭代。 | 导入一周数据后自动生成复盘 Artifact。 | +| P4 企业启用 | Cloud catalog、tenant enablement、overlay、secrets。 | 租户可启用 App,客户数据不进入官方包。 | ## Readiness 规则 -| 检查 | 级别 | 说明 | -|---|---|---| -| `lime.storage` 缺失 | blocking | App 无法保存项目和内容资产。 | -| `lime.files` 缺失 | blocking | 无法上传 / 解析资料。 | -| `project_knowledge` 未绑定 | blocking | 内容生产不能运行。 | -| 必需 writer skill 缺失 | blocking | 批量文案和脚本不可运行。 | -| document parser 缺失 | warning / blocking | P0 上传资料流程 blocking;手工录入可降级。 | -| eval 未启用 | warning | 可生成草稿,但不能标记 publish-ready。 | -| 外部调研工具缺失 | warning | 策略报告降级为用户提供资料模式。 | +| 检查 | 级别 | 说明 | +| -------------------------- | ------------------ | ------------------------------------------ | +| `lime.storage` 缺失 | blocking | App 无法保存项目和内容资产。 | +| `lime.files` 缺失 | blocking | 无法上传 / 解析资料。 | +| `project_knowledge` 未绑定 | blocking | 内容生产不能运行。 | +| 必需 writer skill 缺失 | blocking | 批量文案和脚本不可运行。 | +| document parser 缺失 | warning / blocking | P0 上传资料流程 blocking;手工录入可降级。 | +| eval 未启用 | warning | 可生成草稿,但不能标记 publish-ready。 | +| 外部调研工具缺失 | warning | 策略报告降级为用户提供资料模式。 | ## 验收标准 @@ -420,15 +426,15 @@ P0 不做: ## P0 用户故事 -| 编号 | 用户故事 | 验收标准 | -|---|---|---| -| US-01 | 作为运营,我可以创建一个内容项目,并选择行业、目标平台和是否需要 IP 库。 | `projects` 写入 App storage namespace;不新增 Lime Core 业务表。 | -| US-02 | 作为运营,我可以上传产品资料,系统生成三层知识库草稿。 | 文件通过 `lime.files` 授权读取;结构化结果进入 `knowledge_items`。 | -| US-03 | 作为负责人,我可以逐条确认、裁剪和版本化知识条目。 | 知识库健康度显示字数、分区、来源和版本。 | -| US-04 | 作为内容同学,我可以基于确认后的项目知识库生成 120+ 场景。 | `content_scenarios` 表保存应用场景、用户痛点、解决方案、决策阶段。 | -| US-05 | 作为内容同学,我可以选择内容场景和平台批量生成 20 条文案或脚本。 | 生成结果进入 `content_assets`,带 A/B/C 分级和去 AI 味评分。 | -| US-06 | 作为交付顾问,我可以把内容批次保存为 Artifact 并追溯来源。 | Artifact 带 app / knowledge / skill / tool / eval provenance。 | -| US-07 | 作为管理员,我可以看到缺失能力或缺失知识绑定的 readiness 提示。 | 缺少 `project_knowledge`、`lime.files` 或 writer skill 时不能静默运行。 | +| 编号 | 用户故事 | 验收标准 | +| ----- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------- | +| US-01 | 作为运营,我可以创建一个内容项目,并选择行业、目标平台和是否需要 IP 库。 | `projects` 写入 App storage namespace;不新增 Lime Core 业务表。 | +| US-02 | 作为运营,我可以上传产品资料,系统生成三层知识库草稿。 | 文件通过 `lime.files` 授权读取;结构化结果进入 `knowledge_items`。 | +| US-03 | 作为负责人,我可以逐条确认、裁剪和版本化知识条目。 | 知识库健康度显示字数、分区、来源和版本。 | +| US-04 | 作为内容同学,我可以基于确认后的项目知识库生成 120+ 场景。 | `content_scenarios` 表保存应用场景、用户痛点、解决方案、决策阶段。 | +| US-05 | 作为内容同学,我可以选择内容场景和平台批量生成 20 条文案或脚本。 | 生成结果进入 `content_assets`,带 A/B/C 分级和去 AI 味评分。 | +| US-06 | 作为交付顾问,我可以把内容批次保存为 Artifact 并追溯来源。 | Artifact 带 app / knowledge / skill / tool / eval provenance。 | +| US-07 | 作为管理员,我可以看到缺失能力或缺失知识绑定的 readiness 提示。 | 缺少 `project_knowledge`、`lime.files` 或 writer skill 时不能静默运行。 | ## 典型用例 @@ -458,26 +464,26 @@ P0 不做: ## 实现边界 -| 责任 | 放在 App 实现 | 放在 Lime 平台 | -|---|---|---| -| 三层知识库 UI | 是 | 提供 UI host 和 storage capability。 | -| 文案生成业务规则 | 是 | 提供 Agent Runtime、Skills 调用、Evidence。 | -| 去 AI 味规则库 | 是 | 提供 Eval 执行和结果记录。 | -| 文件权限与解析入口 | 编排 | `lime.files` 和 `lime.tools` 提供授权与工具执行。 | -| 数据表物理存储 | 否 | `lime.storage` 管 namespace、CRUD、migration。 | -| Artifact 持久化 | 定义内容 | `lime.artifacts` 管创建、打开、导出和 provenance。 | -| 凭证 | 否 | `lime.secrets` 托管。 | -| 租户默认配置 | App 默认值 | Cloud / Desktop overlay 覆盖。 | +| 责任 | 放在 App 实现 | 放在 Lime 平台 | +| ------------------ | ------------- | -------------------------------------------------- | +| 三层知识库 UI | 是 | 提供 UI host 和 storage capability。 | +| 文案生成业务规则 | 是 | 提供 Agent Runtime、Skills 调用、Evidence。 | +| 去 AI 味规则库 | 是 | 提供 Eval 执行和结果记录。 | +| 文件权限与解析入口 | 编排 | `lime.files` 和 `lime.tools` 提供授权与工具执行。 | +| 数据表物理存储 | 否 | `lime.storage` 管 namespace、CRUD、migration。 | +| Artifact 持久化 | 定义内容 | `lime.artifacts` 管创建、打开、导出和 provenance。 | +| 凭证 | 否 | `lime.secrets` 托管。 | +| 租户默认配置 | App 默认值 | Cloud / Desktop overlay 覆盖。 | ## P0 页面验收细节 -| 页面 | 必备组件 | Capability | -|---|---|---| -| 项目首页 | 项目列表、知识健康度、最近 Artifact、快捷操作。 | `lime.storage`、`lime.artifacts` | -| 三层知识库 | IP / 项目 / 素材 tabs、分区表、字数警告、来源查看。 | `lime.files`、`lime.storage`、`lime.knowledge` | -| 内容场景规划 | 内容场景表、维度筛选、人工编辑、批量保存。 | `lime.agent`、`lime.storage` | -| 内容工厂 | 平台选择、数量、内容场景选择、批量结果、评分与分级。 | `lime.agent`、`lime.artifacts`、`lime.evidence` | -| 交付物 | 内容表、报告、导出、版本历史。 | `lime.artifacts`、`lime.evidence` | +| 页面 | 必备组件 | Capability | +| ------------ | ---------------------------------------------------- | ----------------------------------------------- | +| 项目首页 | 项目列表、知识健康度、最近 Artifact、快捷操作。 | `lime.storage`、`lime.artifacts` | +| 三层知识库 | IP / 项目 / 素材 tabs、分区表、字数警告、来源查看。 | `lime.files`、`lime.storage`、`lime.knowledge` | +| 内容场景规划 | 内容场景表、维度筛选、人工编辑、批量保存。 | `lime.agent`、`lime.storage` | +| 内容工厂 | 平台选择、数量、内容场景选择、批量结果、评分与分级。 | `lime.agent`、`lime.artifacts`、`lime.evidence` | +| 交付物 | 内容表、报告、导出、版本历史。 | `lime.artifacts`、`lime.evidence` | ## 暂缓项 diff --git a/docs/roadmap/agentapp/implementation-plan.md b/docs/roadmap/agentapp/implementation-plan.md index bc6c8965d..28faab644 100644 --- a/docs/roadmap/agentapp/implementation-plan.md +++ b/docs/roadmap/agentapp/implementation-plan.md @@ -39,15 +39,15 @@ Agent App 不是专家卡片、不是 Prompt 包、不是 Markdown 目录,也 ## 目标 -| 目标 | 说明 | -|---|---| -| 验证 App package | 本地读取 Agent App v0.3+ package / fixture,生成 manifest、projection、readiness。 | -| 验证 SDK 边界 | App 只能通过 `@lime/app-sdk` capability facade 调 Lime 能力,不能 import Lime internal modules。 | -| 验证真实业务切片 | 用 内容工厂跑通一个最小业务闭环,而不是停留在专家聊天。 | -| 验证 App 内 Agent 任务 | 在 App 页面 / workflow 内启动、观察、取消、重试、确认 Agent task,并把结构化结果写回业务对象。 | -| 验证本地数据隔离 | App storage、artifact、evidence、log、package cache 独立命名空间,可按 app 清理。 | -| 验证失败退出 | P0 就实现 cleanup plan / uninstall plan,保证失败后能删干净。 | -| 验证未来 Cloud 对接 | 客户端先用 local JSON / fixture,Cloud 只在 P5 作为 catalog / release / tenant enablement 输入。 | +| 目标 | 说明 | +| ---------------------- | ------------------------------------------------------------------------------------------------ | +| 验证 App package | 本地读取 Agent App v0.3+ package / fixture,生成 manifest、projection、readiness。 | +| 验证 SDK 边界 | App 只能通过 `@lime/app-sdk` capability facade 调 Lime 能力,不能 import Lime internal modules。 | +| 验证真实业务切片 | 用 内容工厂跑通一个最小业务闭环,而不是停留在专家聊天。 | +| 验证 App 内 Agent 任务 | 在 App 页面 / workflow 内启动、观察、取消、重试、确认 Agent task,并把结构化结果写回业务对象。 | +| 验证本地数据隔离 | App storage、artifact、evidence、log、package cache 独立命名空间,可按 app 清理。 | +| 验证失败退出 | P0 就实现 cleanup plan / uninstall plan,保证失败后能删干净。 | +| 验证未来 Cloud 对接 | 客户端先用 local JSON / fixture,Cloud 只在 P5 作为 catalog / release / tenant enablement 输入。 | ## 非目标 @@ -63,17 +63,17 @@ Agent App 不是专家卡片、不是 Prompt 包、不是 Markdown 目录,也 ## 总体原则 -| 原则 | 落地方式 | -|---|---| -| 实验岛优先 | 代码集中在 `src/features/agent-app/`,主系统只保留 feature flag 入口。 | -| 先静态后运行 | 先做 manifest / projection / readiness,不执行 UI bundle 或 worker。 | -| 先 mock 后 adapter | SDK 先接 mock host,确认接口稳定后再接少量真实能力。 | -| 先本地后 Cloud | P0-P4 使用本地 fixture,P5 才消费 LimeCore bootstrap payload。 | -| 先可删除后扩展 | 安装、运行、产物、日志都必须有 cleanup plan。 | -| SDK 暴露能力 | App 只调用 capability,不知道 Lime 内部 store、command、runtime 路径。 | -| 产物必须可追溯 | Artifact / Evidence / Task / Log 都带 `sourceKind: agent_app` provenance。 | -| 业务不出 App | App 页面内承载任务进度、引用、工具调用、失败、人工确认和结构化写回。 | -| Agent 不出治理 | `lime.agent`、`lime.workflow`、`lime.tools`、`lime.knowledge` 等能力统一走 Host Bridge / Capability SDK / policy。 | +| 原则 | 落地方式 | +| ------------------ | ------------------------------------------------------------------------------------------------------------------ | +| 实验岛优先 | 代码集中在 `src/features/agent-app/`,主系统只保留 feature flag 入口。 | +| 先静态后运行 | 先做 manifest / projection / readiness,不执行 UI bundle 或 worker。 | +| 先 mock 后 adapter | SDK 先接 mock host,确认接口稳定后再接少量真实能力。 | +| 先本地后 Cloud | P0-P4 使用本地 fixture,P5 才消费 LimeCore bootstrap payload。 | +| 先可删除后扩展 | 安装、运行、产物、日志都必须有 cleanup plan。 | +| SDK 暴露能力 | App 只调用 capability,不知道 Lime 内部 store、command、runtime 路径。 | +| 产物必须可追溯 | Artifact / Evidence / Task / Log 都带 `sourceKind: agent_app` provenance。 | +| 业务不出 App | App 页面内承载任务进度、引用、工具调用、失败、人工确认和结构化写回。 | +| Agent 不出治理 | `lime.agent`、`lime.workflow`、`lime.tools`、`lime.knowledge` 等能力统一走 Host Bridge / Capability SDK / policy。 | ## 仓库边界 @@ -97,10 +97,10 @@ Agent App 不是专家卡片、不是 Prompt 包、不是 Markdown 目录,也 三者分工: -| 仓库 | 责任 | 不做什么 | -|---|---|---| -| `agentapp` | 标准、schema、示例包、reference CLI。 | 不实现 Lime Desktop 内部能力。 | -| `lime` | 客户端安装、运行、SDK host、UI host、storage、runtime bridge。 | 不实现 Cloud catalog / tenant 管理。 | +| 仓库 | 责任 | 不做什么 | +| ---------- | ----------------------------------------------------------------------------- | --------------------------------------------------------- | +| `agentapp` | 标准、schema、示例包、reference CLI。 | 不实现 Lime Desktop 内部能力。 | +| `lime` | 客户端安装、运行、SDK host、UI host、storage、runtime bridge。 | 不实现 Cloud catalog / tenant 管理。 | | `limecore` | Catalog、release、hash、license、tenant enablement、gateway、audit metadata。 | 不运行默认 Agent、不渲染 App UI、不管理本地 App storage。 | ## 架构图 @@ -150,23 +150,22 @@ flowchart TD - `Cloud` 只提供元数据,不成为默认 runtime。 - `Agent` 是 App 作用域任务运行时,不是通用 Chat 回跳点;结果必须回到 App storage / Artifact / Evidence。 - ## v0.3 升级后的计划调整 路线图已从早期 `AI 内容工程化` 样板升级为 Agent App v0.3 / `内容工厂`。因此当前计划新增一个 P4-R rebaseline 阶段,作为 P5 Cloud Bootstrap 的前置 gate。 P4-R 的目标不是新增业务范围,而是让代码事实源与升级后的文档事实源重新一致: -| 调整项 | 旧实现 | v0.3 current | -|---|---|---| -| 标准版本 | `manifestVersion: 0.2.x` | `manifestVersion: 0.3.0` | -| Fixture | `content-engineering-app.json` | `content-factory-app.json` | -| App ID | `shenlan-content-engineering` | `content-factory-app` | -| Workflow entry | `scene_exhaustion` | `content_scenario_planning` | -| Demo API | `runContentEngineeringDemo()` | `runContentFactoryDemo()` | -| Storage | `scenes/*` | `content_scenarios/*` | -| Artifact kind | `content_engineering_table` | `content_table` | -| Evidence kind | `content_engineering_demo` | `content_factory_demo` | +| 调整项 | 旧实现 | v0.3 current | +| -------------- | ------------------------------ | --------------------------- | +| 标准版本 | `manifestVersion: 0.2.x` | `manifestVersion: 0.3.0` | +| Fixture | `content-engineering-app.json` | `content-factory-app.json` | +| App ID | `shenlan-content-engineering` | `content-factory-app` | +| Workflow entry | `scene_exhaustion` | `content_scenario_planning` | +| Demo API | `runContentEngineeringDemo()` | `runContentFactoryDemo()` | +| Storage | `scenes/*` | `content_scenarios/*` | +| Artifact kind | `content_engineering_table` | `content_table` | +| Evidence kind | `content_engineering_demo` | `content_factory_demo` | 新的执行顺序: @@ -180,7 +179,7 @@ P4-R0 计划与事实源收口 → P5 Cloud Bootstrap payload 本地适配器 ``` -截至 2026-05-16,P4-R、P5、P6、P7、P8、P9、P10、P11、P12、P13 均已通过对应定向验证;P14 Entry Runtime Guard / Permission Prompt、P15 Lab Install / Launch Flow、P15-H GUI smoke / cleanup rehearsal、P16 Agent App Manager、P16-H multi-app lifecycle hardening、P17 Gate 审计、P17.0、P17.1、P17.2.1-P17.2.5、P17.3 lifecycle / cleanup contract、P17.4.1-P17.4.5、P17.5 formal entry GUI smoke、P18.1 SDK facade / stable error / mock host、P18.2 Host Bridge typed router / stable error response、P18.3 Core capability adapters、P18.4 App-scoped Agent task SDK facade、P18.4-H AgentRuntime handoff gate、P18.5.1 Lime-side 内容工厂 SDK regression、P18.5.2 package-side read-only tests、P18.5-S Host Bridge SDK client、P18.5.3 package-side SDK facade / verify / dist 同步与 P18.6 Raw Worker 前 Gate 已完成当前实现 / 专用验证 / 计划收口;完整 `verify:local` 已于 2026-05-16 07:33 端到端通过,2026-05-16 10:53 SDK seam / handoff core 定向测试 5 files / 17 tests passed,`typecheck`、`test:contracts` 与 `lint` 当前会话复核通过。当前下一刀不再扩 P18 功能面,只做 owner handoff / 提交边界收口。阶段记录详见 [v0.3-rebaseline-plan.md](./v0.3-rebaseline-plan.md)、[p15-lab-install-launch-flow.md](./p15-lab-install-launch-flow.md)、[p15-h-gui-smoke-cleanup-rehearsal.md](./p15-h-gui-smoke-cleanup-rehearsal.md)、[p16-agent-app-manager-product-entry-gate.md](./p16-agent-app-manager-product-entry-gate.md)、[p16-h-multi-app-repository-lifecycle-hardening.md](./p16-h-multi-app-repository-lifecycle-hardening.md)、[p17-formal-entry-gate-audit.md](./p17-formal-entry-gate-audit.md)、[p17-formal-entry-contract.md](./p17-formal-entry-contract.md)、[p17-source-install-contract-hardening.md](./p17-source-install-contract-hardening.md)、[p17-lifecycle-cleanup-contract-hardening.md](./p17-lifecycle-cleanup-contract-hardening.md)、[p17-4-host-bridge-runtime.md](./p17-4-host-bridge-runtime.md)、[p17-5-formal-entry-gui-smoke.md](./p17-5-formal-entry-gui-smoke.md)、[p18-typed-capability-sdk-gate.md](./p18-typed-capability-sdk-gate.md)、[p18-4-h-agentruntime-handoff-gate.md](./p18-4-h-agentruntime-handoff-gate.md)、[p18-5-content-factory-sdk-regression.md](./p18-5-content-factory-sdk-regression.md)、[p18-5-3-package-sdk-migration-plan.md](./p18-5-3-package-sdk-migration-plan.md)、[p18-5-3-owner-handoff.md](./p18-5-3-owner-handoff.md)、[p18-6-raw-worker-pre-gate.md](./p18-6-raw-worker-pre-gate.md) 与 [p18-completion-audit.md](./p18-completion-audit.md)。 +截至 2026-05-16,P4-R、P5、P6、P7、P8、P9、P10、P11、P12、P13 均已通过对应定向验证;P14 Entry Runtime Guard / Permission Prompt、P15 Lab Install / Launch Flow、P15-H GUI smoke / cleanup rehearsal、P16 Agent App Manager、P16-H multi-app lifecycle hardening、P17 Gate 审计、P17.0、P17.1、P17.2.1-P17.2.5、P17.3 lifecycle / cleanup contract、P17.4.1-P17.4.5、P17.5 formal entry GUI smoke、P18.1 SDK facade / stable error / mock host、P18.2 Host Bridge typed router / stable error response、P18.3 Core capability adapters、P18.4 App-scoped Agent task SDK facade、P18.4-H AgentRuntime handoff gate、P18.5.1 Lime-side 内容工厂 SDK regression、P18.5.2 package-side read-only tests、P18.5-S Host Bridge SDK client、P18.5.3 package-side SDK facade / verify / dist 同步与 P18.6 Raw Worker 前 Gate 已完成当前实现 / 专用验证 / 计划收口;完整 `verify:local` 已于 2026-05-16 07:33 端到端通过,2026-05-16 10:53 SDK seam / handoff core 定向测试 5 files / 17 tests passed,`typecheck`、`test:contracts` 与 `lint` 当前会话复核通过。当前主线重新进入 P18.7 全量 Lime capability surface:Agent App v0.6 reference 兼容已完成,下一步进入 `lime.capabilities` Host discovery 与后端能力接线。阶段记录详见 [v0.3-rebaseline-plan.md](./v0.3-rebaseline-plan.md)、[p15-lab-install-launch-flow.md](./p15-lab-install-launch-flow.md)、[p15-h-gui-smoke-cleanup-rehearsal.md](./p15-h-gui-smoke-cleanup-rehearsal.md)、[p16-agent-app-manager-product-entry-gate.md](./p16-agent-app-manager-product-entry-gate.md)、[p16-h-multi-app-repository-lifecycle-hardening.md](./p16-h-multi-app-repository-lifecycle-hardening.md)、[p17-formal-entry-gate-audit.md](./p17-formal-entry-gate-audit.md)、[p17-formal-entry-contract.md](./p17-formal-entry-contract.md)、[p17-source-install-contract-hardening.md](./p17-source-install-contract-hardening.md)、[p17-lifecycle-cleanup-contract-hardening.md](./p17-lifecycle-cleanup-contract-hardening.md)、[p17-4-host-bridge-runtime.md](./p17-4-host-bridge-runtime.md)、[p17-5-formal-entry-gui-smoke.md](./p17-5-formal-entry-gui-smoke.md)、[p18-typed-capability-sdk-gate.md](./p18-typed-capability-sdk-gate.md)、[p18-4-h-agentruntime-handoff-gate.md](./p18-4-h-agentruntime-handoff-gate.md)、[p18-5-content-factory-sdk-regression.md](./p18-5-content-factory-sdk-regression.md)、[p18-5-3-package-sdk-migration-plan.md](./p18-5-3-package-sdk-migration-plan.md)、[p18-5-3-owner-handoff.md](./p18-5-3-owner-handoff.md)、[p18-6-raw-worker-pre-gate.md](./p18-6-raw-worker-pre-gate.md) 与 [p18-completion-audit.md](./p18-completion-audit.md)。 上游 `agentapp` 标准随后补齐了宿主实现视角的 v0.3 细则:manifest / projection / readiness JSON schema、release metadata、overlay resolver、lifecycle、typed SDK expectations、reference CLI cross-check。P5 因此不能只做一个 Cloud payload DTO,而要先把这些标准差距变成客户端可执行计划;详见 [p5-cloud-bootstrap.md](./p5-cloud-bootstrap.md)。 @@ -196,17 +195,17 @@ P4-R0 计划与事实源收口 上游 `/Users/coso/Documents/dev/ai/limecloud/agentapp` 已从“概念标准 + 示例包”升级为带宿主实现指南的 v0.3 标准:新增 discovery / installation、release distribution、runtime model、security model、overlay resolver、readiness runner、typed Capability SDK、public JSON Schema 和 reference CLI。LimeCore 服务端路线图也已明确 control-plane-only 边界:服务端只保存 catalog、release、tenant enablement、policy、ToolHub / Gateway metadata;未激活注册码时不下发 `packageUrl / packageHash / manifestHash`,真实 release 必须是 `https` package URL 和完整 `sha256:<64 hex>` hash。客户端计划不需要推翻 P0-P17 主线,但必须把 P17.2 拆得更细,避免把 Cloud metadata、seeded catalog 或开发态源码目录误当成正式安装。 -| 上游新增约束 | 客户端计划调整 | 阶段归属 | -|---|---|---| -| Release 必须 pin 到具体 `packageUrl / packageHash / manifestHash / compatibility / signatureRef?`。 | Cloud release 先归一化为 `AgentAppReleaseDescriptor`,再进入 P12 cache / verify;缺 hash、hash mismatch、manifest mismatch 均阻断。 | P17.2.4 | -| 安装审查基于 projection,不执行 App code。 | `installReview` 只能消费 verified package projection、readiness summary、permission / storage / cleanup 摘要;Cloud card 和 Lab fixture 不能直接生成 production review。 | P17.2.4 | -| Cached fallback 只保证已安装版本可用。 | offline / cached 状态只能启动已安装同 hash package;不能在离线时把 Cloud metadata 伪装成新安装。 | P17.2.4 | -| LimeCore 未激活注册码时不下发 package metadata。 | `registration-required / expired / revoked` 必须停在 source state;客户端不能用 seeded fixture、本地开发目录或历史 cache 创建新安装 review。 | P17.2.4b | -| `content-factory-app` seeded catalog 不能带假 release。 | seeded 只允许演示 registration-required;正式 review 必须来自已验证 package source。 | P17.2.4b | -| Public schema 与 reference CLI 成为机械契约。 | 用上游 `docs/public/schemas/*`、`agentapp-ref` 和 `docs/examples/content-factory-app` 做 projection / readiness cross-check,防止客户端字段漂移。 | P17.2.5 | -| Overlay 是 package hash 外的配置。 | P17.3 后 lifecycle / cleanup 必须把 overlay、secret binding、setup state 与 package code 分离;upgrade 不覆盖 overlay。 | P17.3 | -| Runtime surface 必须运行 App 自己的 UI / workflow,并通过 injected SDK。 | 已完成的正式 runtime surface 补丁只能算 P17.4 预铺;P17.4 仍要确认生产路径只加载 verified runtime package,不依赖 dev resolver,并把 SDK bridge / sandbox / provenance 做成可测 gate。 | P17.4 | -| Typed SDK 要有类型、schema、mock 和 contract tests。 | P18 前不得扩 raw worker;先把 `lime.ui / storage / agent / knowledge / tools / artifacts / workflow / policy / secrets / evidence` 的 typed host bridge 形成 SDK gate。 | P18 | +| 上游新增约束 | 客户端计划调整 | 阶段归属 | +| --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| Release 必须 pin 到具体 `packageUrl / packageHash / manifestHash / compatibility / signatureRef?`。 | Cloud release 先归一化为 `AgentAppReleaseDescriptor`,再进入 P12 cache / verify;缺 hash、hash mismatch、manifest mismatch 均阻断。 | P17.2.4 | +| 安装审查基于 projection,不执行 App code。 | `installReview` 只能消费 verified package projection、readiness summary、permission / storage / cleanup 摘要;Cloud card 和 Lab fixture 不能直接生成 production review。 | P17.2.4 | +| Cached fallback 只保证已安装版本可用。 | offline / cached 状态只能启动已安装同 hash package;不能在离线时把 Cloud metadata 伪装成新安装。 | P17.2.4 | +| LimeCore 未激活注册码时不下发 package metadata。 | `registration-required / expired / revoked` 必须停在 source state;客户端不能用 seeded fixture、本地开发目录或历史 cache 创建新安装 review。 | P17.2.4b | +| `content-factory-app` seeded catalog 不能带假 release。 | seeded 只允许演示 registration-required;正式 review 必须来自已验证 package source。 | P17.2.4b | +| Public schema 与 reference CLI 成为机械契约。 | 用上游 `docs/public/schemas/*`、`agentapp-ref` 和 `docs/examples/content-factory-app` 做 projection / readiness cross-check,防止客户端字段漂移。 | P17.2.5 | +| Overlay 是 package hash 外的配置。 | P17.3 后 lifecycle / cleanup 必须把 overlay、secret binding、setup state 与 package code 分离;upgrade 不覆盖 overlay。 | P17.3 | +| Runtime surface 必须运行 App 自己的 UI / workflow,并通过 injected SDK。 | 已完成的正式 runtime surface 补丁只能算 P17.4 预铺;P17.4 仍要确认生产路径只加载 verified runtime package,不依赖 dev resolver,并把 SDK bridge / sandbox / provenance 做成可测 gate。 | P17.4 | +| Typed SDK 要有类型、schema、mock 和 contract tests。 | P18 前不得扩 raw worker;先把全量 `lime.*` capability catalog、typed host bridge、profile 和 SDK facade 形成单一事实源;P18.7 起不再只覆盖内容工厂当前用到的局部能力。 | P18 | P17.2.5 不是重复 P5.5:P5.5 只在 Cloud bootstrap 和早期 projection 层记录标准差异;P17.2.5 要把同一套 schema / CLI 对照接到正式 `agent-apps` source / install / review / readiness 主链上。 @@ -214,15 +213,15 @@ P17.2.5 不是重复 P5.5:P5.5 只在 Cloud bootstrap 和早期 projection 层 上游 `agentapp-ref@0.4.0` 已把 Host Bridge v1 明确为 sandboxed Agent App UI 的标准运行时事件协议。P17.4 已完成 Lime Desktop runtime surface 的 Host Bridge 生产硬化;P17.5 已由独立 GUI smoke 证明正式 `agent-apps` 入口可用。P18 现在可以把 Host Bridge 固定成 App 可依赖的 Typed Capability SDK Gate。 -| 上游 v0.4 新约束 | 客户端计划调整 | 阶段归属 | -|---|---|---| -| Host Bridge v1 负责 theme、locale、snapshot、visibility、toast、navigate、download、external open 和 capability invoke 传输。 | P17.4 已完成运行面预铺;P18.0 先做标准 gap matrix,确认 Lime Desktop 事件名、信封、source / origin 校验与上游一致。 | P18.0 | -| App 作者不应直接手写私有 `postMessage` 协议。 | P18.1 已固定 `@lime/app-sdk` typed facade、capability invoke envelope、stable error enum 和 mock host。 | P18.1 已完成 | -| `capability:invoke` 仍必须经过 manifest、readiness、permission、policy 和 provenance。 | P18.2 已引入 typed router / adapter gate,未知 capability 不能返回 mock 成功,Host error 必须映射到 stable error response。 | P18.2 已完成 | -| `lime.agent` 是 App 内 Agent task 能力,不是回跳通用 Chat 的快捷方式。 | P18.4 已把 start / stream / get / cancel / retry / submitHostResponse / listTasks 包成 typed SDK facade;P18.4-H 已完成 AgentRuntime handoff 运行证据对齐。 | P18.4 / P18.4-H 已完成 | -| Raw worker 需要额外沙箱和资源限制。 | P18 只做 typed workflow / capability invoke;raw worker sandbox 放到后续 gate。 | P18.6 / P19 | +| 上游 v0.4 新约束 | 客户端计划调整 | 阶段归属 | +| ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | +| Host Bridge v1 负责 theme、locale、snapshot、visibility、toast、navigate、download、external open 和 capability invoke 传输。 | P17.4 已完成运行面预铺;P18.0 先做标准 gap matrix,确认 Lime Desktop 事件名、信封、source / origin 校验与上游一致。 | P18.0 | +| App 作者不应直接手写私有 `postMessage` 协议。 | P18.1 已固定 `@lime/app-sdk` typed facade、capability invoke envelope、stable error enum 和 mock host。 | P18.1 已完成 | +| `capability:invoke` 仍必须经过 manifest、readiness、permission、policy 和 provenance。 | P18.2 已引入 typed router / adapter gate,未知 capability 不能返回 mock 成功,Host error 必须映射到 stable error response。 | P18.2 已完成 | +| `lime.agent` 是 App 内 Agent task 能力,不是回跳通用 Chat 的快捷方式。 | P18.4 已把 start / stream / get / cancel / retry / submitHostResponse / listTasks 包成 typed SDK facade;P18.4-H 已完成 AgentRuntime handoff 运行证据对齐。 | P18.4 / P18.4-H 已完成 | +| Raw worker 需要额外沙箱和资源限制。 | P18 只做 typed workflow / capability invoke;raw worker sandbox 放到后续 gate。 | P18.6 / P19 | -协作边界:P17.5 evidence summary 已落地;P18 代码实施继续不启停隔壁 DevBridge / Vite / Tauri,不改 AgentRuntime Rust 投影任务,不把 SDK 设计扩大成 marketplace / raw worker。 +协作边界:P17.5 evidence summary 已落地;P18.7 继续不启停隔壁 DevBridge / Vite / Tauri,不改 AgentRuntime Rust 投影任务,不把 SDK 设计扩大成 marketplace / raw worker。全量能力只做 capability surface 收敛,真实后端接线按 owner 分期推进。 ### 2026-05-16 隔壁 AgentRuntime 更新接入边界 @@ -235,27 +234,28 @@ P17.2.5 不是重复 P5.5:P5.5 只在 Cloud bootstrap 和早期 projection 层 协作分工锁定: -| 工作包 | 本目录 Agent App P18 负责 | 隔壁 AgentRuntime 负责 | 暂不触碰 | -|---|---|---|---| -| P18.1 SDK contract | 已完成:`@lime/app-sdk` typed facade、stable error、mock host、capability invoke envelope、SDK contract tests。 | 提供现有 facade 行为事实,不要求同步改 Rust。 | 未改 `src-tauri/*`、`src/lib/api/agentAppRuntime.ts`、`agentRuntimeCapabilityHost*`。 | -| P18.2 Host Bridge router | 已完成:source / origin / appId / entryKey / requestId / typed envelope / stable error mapping 的 typed gate。 | 继续维护 runtime read model 与 command 四侧事实。 | 未启停隔壁 DevBridge / Vite / Tauri。 | -| P18.3 Core adapters | 已完成:`lime.ui / storage / artifacts / evidence / knowledge / tools` 的 typed adapter、namespace / provenance 测试已落。 | 提供 Artifact / Evidence / Tool / Knowledge 的真实 owner 能力。 | 不新增第二套 store、scanner、installer;typecheck / contracts 已通过。 | -| P18.4 App-scoped Agent task | 已完成:消费 `start / stream / get / cancel / retry / submitHostResponse / listTasks`,固定 SDK 事件与错误语义。 | 负责 push subscribe、workspace patch producer、capability catalog 和 Rust / TS 运行事实测试。 | 不复制 Claw `*_skill_launch.rs`,不建垂直 `content_factory_*` 命令。 | -| P18.4-H AgentRuntime handoff | 已完成:消费隔壁 current MVP 证据,明确 push subscribe、workspace patch producer、capability catalog、真实桌面 GUI smoke 的 owner 与退出条件。 | 继续负责运行事实和 GUI 验证链路。 | Agent App P18 只补 SDK contract / docs,不抢 runtime facade / Rust / smoke 脚本。 | -| P18.5 内容工厂 SDK 回归 | Lime-side SDK regression、Host Bridge SDK client、package-side tests / validate / readiness、P18.5.3 SDK facade、真实 package verify 与 dist 同步均已完成;handoff gate `blockers=none`、`distArtifacts=0`。 | 保证 runtime structured patch / artifact / evidence 可被 App 消费。 | 不复刻内容工厂后端产品,不把 Chat 包成主流程;后续只做 owner handoff / 提交边界。 | -| P18.6 Raw Worker 前 Gate | 已完成:P18 不执行 raw worker、外部代码、网络、文件系统或 secret value;raw worker sandbox 后移 P19。 | P19 前只保留受控 workflow DSL 和 typed capability SDK。 | 不新增 worker runtime、不执行任意 package JS。 | +| 工作包 | 本目录 Agent App P18 负责 | 隔壁 AgentRuntime 负责 | 暂不触碰 | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| P18.1 SDK contract | 已完成:`@lime/app-sdk` typed facade、stable error、mock host、capability invoke envelope、SDK contract tests。 | 提供现有 facade 行为事实,不要求同步改 Rust。 | 未改 `src-tauri/*`、`src/lib/api/agentAppRuntime.ts`、`agentRuntimeCapabilityHost*`。 | +| P18.2 Host Bridge router | 已完成:source / origin / appId / entryKey / requestId / typed envelope / stable error mapping 的 typed gate。 | 继续维护 runtime read model 与 command 四侧事实。 | 未启停隔壁 DevBridge / Vite / Tauri。 | +| P18.3 Core adapters | 已完成:`lime.ui / storage / artifacts / evidence / knowledge / tools` 的 typed adapter、namespace / provenance 测试已落。 | 提供 Artifact / Evidence / Tool / Knowledge 的真实 owner 能力。 | 不新增第二套 store、scanner、installer;typecheck / contracts 已通过。 | +| P18.4 App-scoped Agent task | 已完成:消费 `start / stream / get / cancel / retry / submitHostResponse / listTasks`,固定 SDK 事件与错误语义。 | 负责 push subscribe、workspace patch producer、capability catalog 和 Rust / TS 运行事实测试。 | 不复制 Claw `*_skill_launch.rs`,不建垂直 `content_factory_*` 命令。 | +| P18.4-H AgentRuntime handoff | 已完成:消费隔壁 current MVP 证据,明确 push subscribe、workspace patch producer、capability catalog、真实桌面 GUI smoke 的 owner 与退出条件。 | 继续负责运行事实和 GUI 验证链路。 | Agent App P18 只补 SDK contract / docs,不抢 runtime facade / Rust / smoke 脚本。 | +| P18.5 内容工厂 SDK 回归 | Lime-side SDK regression、Host Bridge SDK client、package-side tests / validate / readiness、P18.5.3 SDK facade、真实 package verify 与 dist 同步均已完成;handoff gate `blockers=none`、`distArtifacts=0`。 | 保证 runtime structured patch / artifact / evidence 可被 App 消费。 | 不复刻内容工厂后端产品,不把 Chat 包成主流程;该阶段已收口,后续主线转入 P18.7 capability surface。 | +| P18.6 Raw Worker 前 Gate | 已完成:P18 不执行 raw worker、外部代码、网络、文件系统或 secret value;raw worker sandbox 后移 P19。 | P19 前只保留受控 workflow DSL 和 typed capability SDK。 | 不新增 worker runtime、不执行任意 package JS。 | +| P18.7 Full Lime Capability Surface | 文档先行:`p18-7-full-lime-capability-surface.md` 固定全量 `lime.*` 能力、App / Lime 主 App 边界、v0.6 标准兼容、Host discovery 和后端接线顺序;catalog / SDK / profile 第一刀已落。 | AgentRuntime / ToolRuntime / Desktop Host 分别承接模型、用量、Skills、MCP、浏览器、搜索、媒体、终端、记忆、证据等真实 owner。 | 不新增垂直 App 专用命令,不把 preview 能力 mock 成生产成功。 | ### P14 / P15 / P16 计划更新 上游 Agent App 标准进一步把宿主最低职责收敛成 `Discover → Validate → Project → Check readiness → Authorize → Inject capabilities → Isolate data → Clean up`。P0-P13 已覆盖到 `Check readiness` 与 runtime package loader,缺口集中在 `Authorize` 和 Lab 端到端编排: -| 阶段 | 新定位 | 计划影响 | -|---|---|---| -| P14 | Entry Runtime Guard / Permission Prompt。 | 先把 entry 运行前授权做成硬 gate,合并 readiness、setup、package verification、runtime policy 和用户授权。 | -| P15 | Lab Install / Launch Flow。 | 已串联 install review、verify/cache、installed state、permission prompt、runtime launch 和 cleanup preview。 | -| P15-H | GUI Smoke / Cleanup Rehearsal。 | 已补 Agent App Lab 专用 GUI smoke 与证据输出;全局 GUI smoke 的外部 provider/model 缺口不伪装为 Agent App 失败。 | -| P16 | Agent App Manager / Product Entry Gate。 | 已在实验岛内完成最小 installed app 状态、entry launcher、enable / disable / uninstall preview 和 cleanup evidence;详见 [p16-agent-app-manager-product-entry-gate.md](./p16-agent-app-manager-product-entry-gate.md)。 | -| P16-H | Multi-app repository / lifecycle hardening。 | P16-H.5 已完成最小实现;P17 Gate 已完成审计。 | +| 阶段 | 新定位 | 计划影响 | +| ----- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| P14 | Entry Runtime Guard / Permission Prompt。 | 先把 entry 运行前授权做成硬 gate,合并 readiness、setup、package verification、runtime policy 和用户授权。 | +| P15 | Lab Install / Launch Flow。 | 已串联 install review、verify/cache、installed state、permission prompt、runtime launch 和 cleanup preview。 | +| P15-H | GUI Smoke / Cleanup Rehearsal。 | 已补 Agent App Lab 专用 GUI smoke 与证据输出;全局 GUI smoke 的外部 provider/model 缺口不伪装为 Agent App 失败。 | +| P16 | Agent App Manager / Product Entry Gate。 | 已在实验岛内完成最小 installed app 状态、entry launcher、enable / disable / uninstall preview 和 cleanup evidence;详见 [p16-agent-app-manager-product-entry-gate.md](./p16-agent-app-manager-product-entry-gate.md)。 | +| P16-H | Multi-app repository / lifecycle hardening。 | P16-H.5 已完成最小实现;P17 Gate 已完成审计。 | 这次调整的关键是:P14 是 P15 的前置 gate;P15 / P15-H 已复用 guard,不能为了 GUI smoke、后续演示或正式入口直接调用 loader 或 CapabilityHost。P16 也必须先留在实验岛内验证多 App 生命周期,不能跳到 marketplace 或主导航。 @@ -263,21 +263,21 @@ P17.2.5 不是重复 P5.5:P5.5 只在 Cloud bootstrap 和早期 projection 层 P16-H 不是“把 Manager 做大”,而是把 P16 已证明的单 App 管理闭环压到平台级最小可靠性:多 App 列表、生命周期持久化、清理演练证据和残留检查都必须复用同一套 current 事实源。 -| 顺序 | 交付 | 事实源 / 约束 | 验收 | -|---|---|---|---| -| P16-H.0 | 已完成:计划收口。 | `p16-h-multi-app-repository-lifecycle-hardening.md` 成为 P16-H 详细计划。 | README、implementation plan、P16 文档都指向同一下一刀。 | -| P16-H.1 | 已完成最小实现:Repository-backed multi-app list。 | 只读 P11 `LocalInstalledAgentAppStateRepository`;React state 只做展示缓存。 | Manager 能展示多个 installed app,并可选择当前 App。 | -| P16-H.2 | 已完成最小实现:Selected app launcher + persisted lifecycle。 | launch 继续走 P14 guard;disable / enable 继续走 repository lifecycle API。 | disabled App 不能旁路启动,刷新后状态不丢。 | -| P16-H.3 | 已完成最小实现:Cleanup rehearsal evidence export。 | 复用 P15 uninstall / cleanup preview,不执行真实 delete-data。 | 导出 summary 包含 appId、version、hash、strategy、targets、blockedTargets、timestamp。 | -| P16-H.4 | 已完成最小实现:Residual audit。 | 只检查 Agent App namespace,不扫描 Lime 主业务数据。 | 区分 retained、pending deletion、blocked out-of-scope、repository issue。 | -| P16-H.5 | 已完成最小实现:GUI smoke + flag-off regression。 | 只扩展 Agent App Lab 专用 smoke,不接主导航。 | summary 覆盖 multi-app list、selected app、disable blocker、cleanup evidence、residual audit 与 flag-off。 | -| P17 Gate | 已完成:正式入口前完成度审计。 | 只做审计与缺口登记,不直接发布 marketplace 或主导航。 | Gate checklist 已逐项映射 P16-H 证据与剩余缺口。 | -| P17.0 | 已完成计划收口:Formal Entry Contract。 | 只定义正式 Agent Apps 受控入口契约,不扩 marketplace / Cloud 管理台 / 真实 delete-data。 | 已写清 `agent-apps`、`agent-app-lab` 与 runtime surface 的责任边界、禁区、架构和后续 gates。 | -| P17.1 | 已完成最小实现:Formal route / nav / copy hardening。 | 只硬化正式入口路由、导航、状态文案和测试契约。 | `agent-apps` 不依赖 Lab flag;UI entry 从正式入口进入独立 runtime surface;正式入口文案五语言覆盖。 | -| P17.2 | 已完成最小实现:Source / install contract hardening。 | P17.2.1 / P17.2.2 / P17.2.3 已完成 source state model、install review descriptor 与 registration hardening;P17.2.4a 已完成 Cloud release descriptor / verification gate;P17.2.4b-1 已完成 acquisition seam / verified cache source;P17.2.4b-2 已完成 packageUrl fetch / staging / manifest extraction;P17.2.5 已完成 schema / reference CLI / example package cross-check。详见 [p17-source-install-contract-hardening.md](./p17-source-install-contract-hardening.md)。 | local / cloud 安装已先 review 后 save,registration-active refresh 已有 UI 断言;packageUrl fetch 可生成 verified cache 后再进入 Cloud review;reference CLI cross-check 已覆盖 projection / readiness / review descriptor。 | -| P17.3 | 已完成最小实现:Lifecycle / cleanup contract hardening。 | P17.3.1-P17.3.6 已完成 lifecycle descriptor、formal page lifecycle UI、cleanup namespace classifier、evidence / residual audit、guard integration 与 boundary regression。详见 [p17-lifecycle-cleanup-contract-hardening.md](./p17-lifecycle-cleanup-contract-hardening.md)。 | disabled / cleanup-blocked 不能启动;uninstall 保持 rehearsal-only;evidence 不含 secret value;residual audit 只查 Agent App namespace。 | -| P17.4 | 已完成:Runtime surface production hardening。 | P17.4.1-P17.4.5 已完成 guard-before-start、Host Bridge task contract、structured write-back guard、content factory bootstrap sample 与完整 GUI smoke。详见 [p17-4-host-bridge-runtime.md](./p17-4-host-bridge-runtime.md)。 | code-level tests、typecheck、contracts、feature island boundary 与 `verify:gui-smoke` 均有证据;runtime surface 已支撑 P17.5 formal smoke。 | -| P17.5 | 已完成:Formal entry GUI smoke。 | `smoke:agent-apps` 已覆盖正式 `agent-apps` install / registration / launch / disable / uninstall rehearsal / runtime surface / flag-off。详见 [p17-5-formal-entry-gui-smoke.md](./p17-5-formal-entry-gui-smoke.md)。 | 独立 summary 已输出到 `.lime/qc/gui-evidence/agent-apps/agent-apps-smoke-summary.json`;Lab smoke 仍只作研发辅助证据。 | +| 顺序 | 交付 | 事实源 / 约束 | 验收 | +| -------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| P16-H.0 | 已完成:计划收口。 | `p16-h-multi-app-repository-lifecycle-hardening.md` 成为 P16-H 详细计划。 | README、implementation plan、P16 文档都指向同一下一刀。 | +| P16-H.1 | 已完成最小实现:Repository-backed multi-app list。 | 只读 P11 `LocalInstalledAgentAppStateRepository`;React state 只做展示缓存。 | Manager 能展示多个 installed app,并可选择当前 App。 | +| P16-H.2 | 已完成最小实现:Selected app launcher + persisted lifecycle。 | launch 继续走 P14 guard;disable / enable 继续走 repository lifecycle API。 | disabled App 不能旁路启动,刷新后状态不丢。 | +| P16-H.3 | 已完成最小实现:Cleanup rehearsal evidence export。 | 复用 P15 uninstall / cleanup preview,不执行真实 delete-data。 | 导出 summary 包含 appId、version、hash、strategy、targets、blockedTargets、timestamp。 | +| P16-H.4 | 已完成最小实现:Residual audit。 | 只检查 Agent App namespace,不扫描 Lime 主业务数据。 | 区分 retained、pending deletion、blocked out-of-scope、repository issue。 | +| P16-H.5 | 已完成最小实现:GUI smoke + flag-off regression。 | 只扩展 Agent App Lab 专用 smoke,不接主导航。 | summary 覆盖 multi-app list、selected app、disable blocker、cleanup evidence、residual audit 与 flag-off。 | +| P17 Gate | 已完成:正式入口前完成度审计。 | 只做审计与缺口登记,不直接发布 marketplace 或主导航。 | Gate checklist 已逐项映射 P16-H 证据与剩余缺口。 | +| P17.0 | 已完成计划收口:Formal Entry Contract。 | 只定义正式 Agent Apps 受控入口契约,不扩 marketplace / Cloud 管理台 / 真实 delete-data。 | 已写清 `agent-apps`、`agent-app-lab` 与 runtime surface 的责任边界、禁区、架构和后续 gates。 | +| P17.1 | 已完成最小实现:Formal route / nav / copy hardening。 | 只硬化正式入口路由、导航、状态文案和测试契约。 | `agent-apps` 不依赖 Lab flag;UI entry 从正式入口进入独立 runtime surface;正式入口文案五语言覆盖。 | +| P17.2 | 已完成最小实现:Source / install contract hardening。 | P17.2.1 / P17.2.2 / P17.2.3 已完成 source state model、install review descriptor 与 registration hardening;P17.2.4a 已完成 Cloud release descriptor / verification gate;P17.2.4b-1 已完成 acquisition seam / verified cache source;P17.2.4b-2 已完成 packageUrl fetch / staging / manifest extraction;P17.2.5 已完成 schema / reference CLI / example package cross-check。详见 [p17-source-install-contract-hardening.md](./p17-source-install-contract-hardening.md)。 | local / cloud 安装已先 review 后 save,registration-active refresh 已有 UI 断言;packageUrl fetch 可生成 verified cache 后再进入 Cloud review;reference CLI cross-check 已覆盖 projection / readiness / review descriptor。 | +| P17.3 | 已完成最小实现:Lifecycle / cleanup contract hardening。 | P17.3.1-P17.3.6 已完成 lifecycle descriptor、formal page lifecycle UI、cleanup namespace classifier、evidence / residual audit、guard integration 与 boundary regression。详见 [p17-lifecycle-cleanup-contract-hardening.md](./p17-lifecycle-cleanup-contract-hardening.md)。 | disabled / cleanup-blocked 不能启动;uninstall 保持 rehearsal-only;evidence 不含 secret value;residual audit 只查 Agent App namespace。 | +| P17.4 | 已完成:Runtime surface production hardening。 | P17.4.1-P17.4.5 已完成 guard-before-start、Host Bridge task contract、structured write-back guard、content factory bootstrap sample 与完整 GUI smoke。详见 [p17-4-host-bridge-runtime.md](./p17-4-host-bridge-runtime.md)。 | code-level tests、typecheck、contracts、feature island boundary 与 `verify:gui-smoke` 均有证据;runtime surface 已支撑 P17.5 formal smoke。 | +| P17.5 | 已完成:Formal entry GUI smoke。 | `smoke:agent-apps` 已覆盖正式 `agent-apps` install / registration / launch / disable / uninstall rehearsal / runtime surface / flag-off。详见 [p17-5-formal-entry-gui-smoke.md](./p17-5-formal-entry-gui-smoke.md)。 | 独立 summary 已输出到 `.lime/qc/gui-evidence/agent-apps/agent-apps-smoke-summary.json`;Lab smoke 仍只作研发辅助证据。 | P17.5 完成只证明正式入口独立 smoke 可交付;marketplace、Workspace pin、命令面板入口、Chat expert entry、真实 delete-data 和 Cloud 管理台仍不进入当前主线。 @@ -461,7 +461,7 @@ src/features/agent-app/ ```ts if (featureFlags.agentAppHost.labEnabled) { - registerAgentAppLabEntry() + registerAgentAppLabEntry(); } ``` @@ -515,18 +515,18 @@ agentAppHost: { } ``` -| 开关 | 启用内容 | 默认 | 失败止血 | -|---|---|---|---| -| `labEnabled` | 显示 Agent App Lab 页面。 | off | UI 入口消失。 | -| `localPackageEnabled` | 读取本地 package / fixture。 | off | 不扫描本地 App。 | -| `projectionEnabled` | 生成 projection。 | off | 不产生派生对象。 | -| `readinessEnabled` | 检查 capability / permission / runtime。 | off | 不做启用判断。 | -| `mockSdkEnabled` | 使用 mock capability host。 | off | 不运行 mock action。 | -| `localStorageEnabled` | 创建 App namespace。 | off | 不写本地实验数据。 | -| `realAdapterEnabled` | 接入少量真实 Lime adapter。 | off | 回退 mock。 | -| `uiRuntimeEnabled` | 加载受控 UI bundle。 | off | 回退只读 projection。 | -| `workerRuntimeEnabled` | 执行受控 workflow DSL,仍不执行 raw worker bundle。 | off | 不跑后台任务。 | -| `cloudBootstrapEnabled` | 消费 LimeCore bootstrap。 | off | 只用本地 fixture。 | +| 开关 | 启用内容 | 默认 | 失败止血 | +| ----------------------- | --------------------------------------------------- | ---- | --------------------- | +| `labEnabled` | 显示 Agent App Lab 页面。 | off | UI 入口消失。 | +| `localPackageEnabled` | 读取本地 package / fixture。 | off | 不扫描本地 App。 | +| `projectionEnabled` | 生成 projection。 | off | 不产生派生对象。 | +| `readinessEnabled` | 检查 capability / permission / runtime。 | off | 不做启用判断。 | +| `mockSdkEnabled` | 使用 mock capability host。 | off | 不运行 mock action。 | +| `localStorageEnabled` | 创建 App namespace。 | off | 不写本地实验数据。 | +| `realAdapterEnabled` | 接入少量真实 Lime adapter。 | off | 回退 mock。 | +| `uiRuntimeEnabled` | 加载受控 UI bundle。 | off | 回退只读 projection。 | +| `workerRuntimeEnabled` | 执行受控 workflow DSL,仍不执行 raw worker bundle。 | off | 不跑后台任务。 | +| `cloudBootstrapEnabled` | 消费 LimeCore bootstrap。 | off | 只用本地 fixture。 | ## 数据边界 @@ -558,16 +558,16 @@ P0 使用独立物理目录,不混入现有主业务表: ```ts type AgentAppProvenance = { - sourceKind: 'agent_app' - appId: string - appVersion: string - packageHash: string - manifestHash: string - entryKey?: string - workflowRunId?: string - workspaceId?: string - taskId?: string -} + sourceKind: "agent_app"; + appId: string; + appVersion: string; + packageHash: string; + manifestHash: string; + entryKey?: string; + workflowRunId?: string; + workspaceId?: string; + taskId?: string; +}; ``` 禁止: @@ -655,16 +655,16 @@ Projection 是安装时生成的只读派生对象,不是全局注册: ```ts type AgentAppProjection = { - app: AppSummary - package: PackageIdentity - entries: ProjectedEntry[] - requiredCapabilities: CapabilityRequirement[] - storage?: StorageProjection - artifacts?: ArtifactProjection[] - policies: PolicyProjection[] - readinessHints: ReadinessHint[] - provenance: AgentAppProvenance -} + app: AppSummary; + package: PackageIdentity; + entries: ProjectedEntry[]; + requiredCapabilities: CapabilityRequirement[]; + storage?: StorageProjection; + artifacts?: ArtifactProjection[]; + policies: PolicyProjection[]; + readinessHints: ReadinessHint[]; + provenance: AgentAppProvenance; +}; ``` 允许: @@ -693,17 +693,17 @@ P0 SDK 先做 facade、错误码、mock host 和 provenance,不绑定内部实 ```ts interface LimeAppSdk { - storage: LimeStorageCapability - files: LimeFilesCapability - agent: LimeAgentCapability - knowledge: LimeKnowledgeCapability - tools: LimeToolsCapability - artifacts: LimeArtifactsCapability - workflow: LimeWorkflowCapability - evidence: LimeEvidenceCapability - policy: LimePolicyCapability - secrets: LimeSecretsCapability - events: LimeEventsCapability + storage: LimeStorageCapability; + files: LimeFilesCapability; + agent: LimeAgentCapability; + knowledge: LimeKnowledgeCapability; + tools: LimeToolsCapability; + artifacts: LimeArtifactsCapability; + workflow: LimeWorkflowCapability; + evidence: LimeEvidenceCapability; + policy: LimePolicyCapability; + secrets: LimeSecretsCapability; + events: LimeEventsCapability; } ``` @@ -739,13 +739,13 @@ P0-P2 不加载 App UI bundle,只用 Lab projection 展示。 P3 才做受控 UI Extension Host: -| 能力 | P3 范围 | 禁止 | -|---|---|---| -| `page` entry | 在受控容器中展示 App 页面。 | 直接注册主路由。 | -| `panel` entry | 在 Lab 中打开侧栏 / 面板。 | 直接改 Workspace layout。 | -| `settings` entry | 展示 App 自己的设置页。 | 修改全局设置结构。 | -| SDK 注入 | Host 注入 scoped handles。 | App import 内部模块。 | -| 权限提示 | UI 解释权限用途。 | 只靠 UI 提示,不做 runtime 拦截。 | +| 能力 | P3 范围 | 禁止 | +| ---------------- | --------------------------- | --------------------------------- | +| `page` entry | 在受控容器中展示 App 页面。 | 直接注册主路由。 | +| `panel` entry | 在 Lab 中打开侧栏 / 面板。 | 直接改 Workspace layout。 | +| `settings` entry | 展示 App 自己的设置页。 | 修改全局设置结构。 | +| SDK 注入 | Host 注入 scoped handles。 | App import 内部模块。 | +| 权限提示 | UI 解释权限用途。 | 只靠 UI 提示,不做 runtime 拦截。 | UI Host 最小要求: @@ -758,15 +758,15 @@ UI Host 最小要求: Worker 是最高风险项,必须晚于 UI Host: -| 阶段 | Runtime 能力 | 是否执行 App 代码 | -|---|---|---| -| P0 | manifest / projection / readiness | 否 | -| P1 | mock entry action | 否,只跑内置 mock handler | -| P2 | thin adapter action | 否,仍由 Lime 内置 runner 调 adapter | -| P3 | 受控 UI bundle | 是,仅 UI 容器 | -| P4.1 | 内容工厂内置 runner | 否,仍由 Lime 内置 demo 编排 | -| P4.2 | 受控 workflow DSL | 否,只执行白名单 SDK step,不执行 raw worker bundle | -| P4.x | 真实 worker sandbox | 是,必须有 policy、cancel、trace、resource limit | +| 阶段 | Runtime 能力 | 是否执行 App 代码 | +| ---- | --------------------------------- | --------------------------------------------------- | +| P0 | manifest / projection / readiness | 否 | +| P1 | mock entry action | 否,只跑内置 mock handler | +| P2 | thin adapter action | 否,仍由 Lime 内置 runner 调 adapter | +| P3 | 受控 UI bundle | 是,仅 UI 容器 | +| P4.1 | 内容工厂内置 runner | 否,仍由 Lime 内置 demo 编排 | +| P4.2 | 受控 workflow DSL | 否,只执行白名单 SDK step,不执行 raw worker bundle | +| P4.x | 真实 worker sandbox | 是,必须有 policy、cancel、trace、resource limit | Worker 禁止项: @@ -802,14 +802,14 @@ P4 最小闭环: 最小业务对象: -| 对象 | 存储位置 | 产物 | -|---|---|---| -| project | App storage namespace | 项目配置。 | -| knowledge binding | App storage + lime.knowledge ref | 三层知识库引用。 | -| content_scenarios | App storage | 内容场景规划表。 | -| content assets | App storage | 文案 / 脚本 / 图片提示词。 | -| content table | Artifact store 或实验 artifact dir | 可导出内容表。 | -| evidence | Evidence store 或实验 evidence dir | 来源、模型、知识版本、App provenance。 | +| 对象 | 存储位置 | 产物 | +| ----------------- | ---------------------------------- | -------------------------------------- | +| project | App storage namespace | 项目配置。 | +| knowledge binding | App storage + lime.knowledge ref | 三层知识库引用。 | +| content_scenarios | App storage | 内容场景规划表。 | +| content assets | App storage | 文案 / 脚本 / 图片提示词。 | +| content table | Artifact store 或实验 artifact dir | 可导出内容表。 | +| evidence | Evidence store 或实验 evidence dir | 来源、模型、知识版本、App provenance。 | 停止条件: @@ -826,29 +826,29 @@ P0 必须同步实现 cleanup plan,不允许“先装上再说”。 ```ts type AppCleanupPlan = { - appId: string - packageHash: string - packageCachePaths: string[] - projectionPaths: string[] - readinessPaths: string[] - storageNamespaces: string[] - artifactRefs: string[] - evidenceRefs: string[] - taskRefs: string[] - secretRefs: string[] - logPaths: string[] - exportPaths: string[] -} + appId: string; + packageHash: string; + packageCachePaths: string[]; + projectionPaths: string[]; + readinessPaths: string[]; + storageNamespaces: string[]; + artifactRefs: string[]; + evidenceRefs: string[]; + taskRefs: string[]; + secretRefs: string[]; + logPaths: string[]; + exportPaths: string[]; +}; ``` 卸载策略: -| 策略 | 行为 | 用途 | -|---|---|---| -| Disable only | 禁用 App,保留 package 和数据。 | 临时止血。 | -| Uninstall keep data | 删除 package / projection / readiness,保留 storage / artifacts。 | 升级失败或重装。 | -| Uninstall delete data | 删除 package、projection、storage、artifacts、evidence、logs。 | 实验失败或用户明确清理。 | -| Export then delete | 先导出 storage / artifacts,再删除本地数据。 | 数据迁移。 | +| 策略 | 行为 | 用途 | +| --------------------- | ----------------------------------------------------------------- | ------------------------ | +| Disable only | 禁用 App,保留 package 和数据。 | 临时止血。 | +| Uninstall keep data | 删除 package / projection / readiness,保留 storage / artifacts。 | 升级失败或重装。 | +| Uninstall delete data | 删除 package、projection、storage、artifacts、evidence、logs。 | 实验失败或用户明确清理。 | +| Export then delete | 先导出 storage / artifacts,再删除本地数据。 | 数据迁移。 | 清理流程: @@ -938,12 +938,12 @@ rg "agent-app|AgentApp|agent_app" src src-tauri docs/roadmap/agentapp 允许接入: -| Capability | Adapter | 原则 | -|---|---|---| -| `lime.artifacts.create` | Artifact 创建 adapter。 | 自动附加 provenance。 | -| `lime.evidence.record` | Evidence 记录 adapter。 | 可按 appId 查询。 | -| `lime.knowledge.search` | Knowledge resolver adapter。 | 只读检索。 | -| `lime.agent.startTask` | 本地 Agent Runtime adapter。 | 可 cancel / trace。 | +| Capability | Adapter | 原则 | +| ----------------------- | ---------------------------- | --------------------- | +| `lime.artifacts.create` | Artifact 创建 adapter。 | 自动附加 provenance。 | +| `lime.evidence.record` | Evidence 记录 adapter。 | 可按 appId 查询。 | +| `lime.knowledge.search` | Knowledge resolver adapter。 | 只读检索。 | +| `lime.agent.startTask` | 本地 Agent Runtime adapter。 | 可 cancel / trace。 | 禁止: @@ -1048,14 +1048,14 @@ P4-R 后 P4.2 current 落地标准: P5 分期以 [p5-cloud-bootstrap.md](./p5-cloud-bootstrap.md) 为准: -| 阶段 | 目标 | 不做什么 | -|---|---|---| -| P5.0 | 标准差距收口:把上游 v0.3 schema、projection、readiness、overlay、lifecycle、typed SDK 变化转成客户端 gap matrix。 | 不改运行代码,不接 Cloud 管理台。 | -| P5.1 | `CloudBootstrapPayload` DTO / parser / validator。 | 不下载 package,不安装,不运行 App。 | -| P5.2 | 统一 Package Source Adapter,把 Cloud release 映射到现有安装链路。 | 不新增第二套 Cloud install flow。 | -| P5.3 | tenant enablement、tool availability、policy defaults 与 local readiness 合并。 | 不允许 Cloud 直接把 App 标记为 ready。 | -| P5.4 | disable、hash mismatch、offline、upgrade、uninstall 回归。 | `enabled=false` 不删除用户数据。 | -| P5.5 | 上游 schema / reference CLI cross-check。 | 不把 reference CLI 输出直接当 Lime runtime 实现。 | +| 阶段 | 目标 | 不做什么 | +| ---- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- | +| P5.0 | 标准差距收口:把上游 v0.3 schema、projection、readiness、overlay、lifecycle、typed SDK 变化转成客户端 gap matrix。 | 不改运行代码,不接 Cloud 管理台。 | +| P5.1 | `CloudBootstrapPayload` DTO / parser / validator。 | 不下载 package,不安装,不运行 App。 | +| P5.2 | 统一 Package Source Adapter,把 Cloud release 映射到现有安装链路。 | 不新增第二套 Cloud install flow。 | +| P5.3 | tenant enablement、tool availability、policy defaults 与 local readiness 合并。 | 不允许 Cloud 直接把 App 标记为 ready。 | +| P5.4 | disable、hash mismatch、offline、upgrade、uninstall 回归。 | `enabled=false` 不删除用户数据。 | +| P5.5 | 上游 schema / reference CLI cross-check。 | 不把 reference CLI 输出直接当 Lime runtime 实现。 | 验收: @@ -1068,67 +1068,67 @@ P5 分期以 [p5-cloud-bootstrap.md](./p5-cloud-bootstrap.md) 为准: ## 任务拆分 -| 阶段 | 任务 | 主要文件 | 验证 | -|---|---|---|---| -| P0.1 | 建立类型和 feature flag。 | `types.ts`、`featureFlag.ts` | typecheck。 | -| P0.2 | manifest parser / normalizer。 | `manifest/*` | parser unit tests。 | -| P0.3 | projection / readiness。 | `projection/*`、`readiness/*` | projection tests。 | -| P0.4 | Lab 只读 UI。 | `ui/*` | UI test / GUI smoke。 | -| P0.5 | cleanup dry-run。 | `install/cleanupPlan.ts` | cleanup tests。 | -| P1.1 | SDK facade。 | `sdk/LimeAppSdk.ts` | type contract。 | -| P1.2 | Mock host。 | `sdk/MockCapabilityHost.ts` | contract tests。 | -| P1.3 | mock artifact / evidence。 | `sdk/*`、`install/*` | run entry test。 | -| P2.1 | storage / artifact / evidence adapter。 | `adapters/*` | adapter tests。 | -| P2.2 | knowledge / agent adapter。 | `adapters/*` | adapter tests。 | -| P3.1 | UI host。 | `runtime/uiExtensionHost.ts` | sandbox smoke。 | -| P4-R0 | 计划与事实源收口。 | `docs/roadmap/agentapp/*` | `git diff --check` + legacy 关键词审计。 | -| P4-R1 | v0.3 manifest / fixture / readiness rebaseline。 | `types.ts`、`manifest/*`、`fixtures/*`、`projection/*`、`readiness/*` | parser / projection / readiness / feature flag tests。 | -| P4-R2 | 内容工厂 demo rename 和数据对象 rebaseline。 | `runtime/contentFactoryDemo.ts`、`adapters/*`、`sdk/*` | content factory / adapter / SDK tests。 | -| P4-R3 | WorkflowRuntimeHost rebaseline 到 `content_factory_demo`。 | `runtime/workflowRuntimeHost.ts`、`runtime/runtimePolicy.ts`、`runtime/workflowRuntimeCapabilityProfile.ts` | workflow runtime tests。 | -| P4-R4 | Lab UI、五语言 i18n、index export rebaseline。 | `ui/AgentAppLabPage.tsx`、`index.ts`、`src/i18n/resources/*/agent.json` | UI test + i18n tests。 | -| P4-R5 | Gate 恢复与旧 SceneApp 不复活。 | `src/features/agent-app/*`、相关 dangling 引用 | `test:contracts`、`verify:local`、GUI smoke。 | -| P4.1 | 内容工厂切片。 | `fixtures/*`、`ui/*` | end-to-end demo。 | -| P4.2 | 受控 workflow runtime。 | `runtime/workflowRuntimeHost.ts`、`runtime/runtimePolicy.ts` | workflow runtime tests / UI test。 | -| P5.0 | v0.3 标准差距收口。 | `docs/roadmap/agentapp/p5-cloud-bootstrap.md`、标准 gap matrix | `git diff --check` + legacy 关键词审计。 | -| P5.1 | Cloud Bootstrap payload DTO / parser / validator。 | `install/cloudBootstrap.ts`、`types.ts`、`featureFlag.ts` | payload parser / feature flag tests。 | -| P5.2 | Bootstrap payload 统一 package source adapter。 | `install/*`、`manifest/*`、`projection/*` | package identity / install preview / projection tests。 | -| P5.3 | Tenant enablement + local readiness 合并。 | `readiness/*`、`projection/*`、`ui/*` | readiness / projection / UI tests。 | -| P5.4 | hash mismatch / disable / offline / upgrade 回归。 | `install/*`、`runtime/*`、`ui/*` | uninstall / workflow runtime / UI tests。 | -| P5.5 | Schema / reference CLI cross-check。 | `docs/roadmap/agentapp/*`、上游 `agentapp` reference CLI | `agentapp-ref validate/project/readiness` + 差异记录。 | -| P6 | v0.3 projection / readiness schema coverage。 | `types.ts`、`manifest/*`、`projection/*`、`readiness/*`、`fixtures/*` | parser / projection / readiness / schema coverage tests。 | -| P7 | 本地 schema / snapshot gate。 | `schema/schemaGate.ts`、`schema/schemaGate.test.ts`、`index.ts` | schema gate + projection / readiness tests。 | -| P8 | setup resolver / `needs-setup` 语义。 | `types.ts`、`readiness/*`、`ui/*`、五语言 `agent.json` | readiness / schema gate / UI / i18n tests。 | -| P9 | installed setup state store。 | `install/setupStateStore.ts`、`install/cleanupPlan.ts`、`sdk/*`、`adapters/*`、`ui/*` | setup state / readiness / cleanup / UI tests。 | -| P10 | installed app state snapshot 与 in-memory store。 | `install/installedAppState.ts`、`types.ts`、`install/cleanupPlan.ts`、`ui/*`、`index.ts` | installed state / setup state / projection / UI tests。 | -| P11 | Local Persistence Adapter。 | `install/installedAppState.ts` 或同目录 repository 文件、`install/setupStateStore.ts`、`install/cleanupPlan.ts`、`ui/*` | repository contract / persistence / cleanup / typecheck / contracts。 | -| P12 | Package Cache / Verify / Rollback。 | `install/packageCache.ts`、`install/packageIdentity.ts`、`install/cloudBootstrap.ts`、`install/cleanupPlan.ts`、`readiness/*` | package cache / hash verify / rollback / readiness tests。 | -| P13 | Runtime Package Loader / UI Bundle Loader。 | `install/packageCache.ts`、`runtime/*`、`projection/*`、`ui/*` | loader / UI host / policy guard tests。 | -| P14 | Entry Runtime Guard / Permission Prompt。 | `runtime/*`、`readiness/*`、`install/*`、`ui/*` | guard / permission prompt / setup / policy tests。 | -| P15 | Lab Install / Launch Flow。 | `install/labInstallFlow.ts`、`runtime/entryRuntimeGuard.ts`、`ui/*`、五语言 `agent.json` | lab flow / guard / package cache / installed state / UI tests。 | -| P15-H | GUI smoke / cleanup rehearsal hardening。 | `scripts/agent-app-lab-smoke.mjs`、`featureFlag.ts`、`ui/*`、`docs/roadmap/agentapp/p15-h-gui-smoke-cleanup-rehearsal.md` | `smoke:agent-app-lab` / typecheck / boundary audit。 | -| P16 | Agent App Manager / Product Entry Gate。 | `ui/AgentAppManagerPanel.tsx`、`ui/AgentAppLabPage.tsx`、`install/labInstallFlow.ts`、`runtime/entryRuntimeGuard.ts` | manager UI / entry launcher / lifecycle evidence / GUI smoke。 | -| P16-H | Multi-app repository / lifecycle hardening。 | `install/installedAppState.ts`、`ui/AgentAppManagerPanel.tsx`、`ui/AgentAppManagerPanel.test.tsx`、`scripts/agent-app-lab-smoke.mjs`、`p16-h-multi-app-repository-lifecycle-hardening.md` | repository-backed multi-app list / selected app launcher / persistent lifecycle / cleanup evidence export / residual audit。 | -| P17 Gate | 正式入口前 gate audit。 | `p17-formal-entry-gate-audit.md`、P16-H evidence、boundary audit | gate checklist / command contracts / typecheck / GUI evidence。 | -| P17.0 | Formal Entry Contract。 | `p17-formal-entry-contract.md`、README、implementation plan | `agent-apps` / `agent-app-lab` / runtime surface 职责边界清晰。 | -| P17.1 | Formal route / nav / copy hardening。 | `AgentAppsPage.tsx`、`AppPageContent.tsx`、navigation、五语言 `agent.json` | formal route / copy / sidebar / i18n / boundary tests。 | -| P17.2 | Source / install contract hardening。 | `p17-source-install-contract-hardening.md`、`AgentAppsPage.tsx`、`src/lib/api/agentApps.ts`、`install/*`、上游 `agentapp` schema / reference CLI | P17.2.1-P17.2.5 已覆盖 source state / install review / registration / release descriptor / verification gate / acquisition seam / verified cache source / packageUrl fetch / staging / reference CLI cross-check。 | -| P17.3 | Lifecycle / cleanup contract hardening。 | `p17-lifecycle-cleanup-contract-hardening.md`、`install/*`、`ui/AgentAppsPage.tsx`、cleanup evidence / residual audit、overlay / setup / secret binding 边界 | disable / enable、uninstall keep-data、delete-data rehearsal、export-then-delete preview 都复用同一 namespace 和 evidence,不执行真实删除。 | -| P17.4 | Runtime surface production hardening。 | `ui/AgentAppRuntimePage.tsx`、`runtime/*`、`src/lib/api/agentApps.ts`、`src-tauri/src/commands/agent_app_cmd.rs` | 当前 runtime surface 补丁只算预铺;生产路径必须只加载 verified package cache / staging,不依赖 dev resolver,并证明 injected SDK / sandbox / provenance 没有被绕过。 | -| P17.5 | Formal entry GUI smoke。 | `scripts/agent-apps-smoke.mjs`、正式 `agent-apps` route、P17 evidence summary、`p17-5-formal-entry-gui-smoke.md` | 已完成正式入口 smoke,覆盖 install / registration / launch / disable / uninstall rehearsal / runtime surface / flag-off;Lab smoke 只保留研发回归。 | -| P18 | Typed Capability SDK gate。 | `docs/roadmap/agentapp/p18-typed-capability-sdk-gate.md`、`p18-5-content-factory-sdk-regression.md`、`p18-5-3-package-sdk-migration-plan.md`、`p18-completion-audit.md`、`sdk/*`、`runtime/*`、schema / mock / contract tests | 在扩 raw worker 前,把 `lime.ui / storage / agent / knowledge / tools / artifacts / workflow / policy / secrets / evidence` 的 typed host bridge 固定为稳定 SDK 契约,并对齐上游 v0.4 Host Bridge v1;P18.5-S 已补 Host Bridge SDK client,P18.5.3 等外部 package 稳定后执行。 | +| 阶段 | 任务 | 主要文件 | 验证 | +| -------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| P0.1 | 建立类型和 feature flag。 | `types.ts`、`featureFlag.ts` | typecheck。 | +| P0.2 | manifest parser / normalizer。 | `manifest/*` | parser unit tests。 | +| P0.3 | projection / readiness。 | `projection/*`、`readiness/*` | projection tests。 | +| P0.4 | Lab 只读 UI。 | `ui/*` | UI test / GUI smoke。 | +| P0.5 | cleanup dry-run。 | `install/cleanupPlan.ts` | cleanup tests。 | +| P1.1 | SDK facade。 | `sdk/LimeAppSdk.ts` | type contract。 | +| P1.2 | Mock host。 | `sdk/MockCapabilityHost.ts` | contract tests。 | +| P1.3 | mock artifact / evidence。 | `sdk/*`、`install/*` | run entry test。 | +| P2.1 | storage / artifact / evidence adapter。 | `adapters/*` | adapter tests。 | +| P2.2 | knowledge / agent adapter。 | `adapters/*` | adapter tests。 | +| P3.1 | UI host。 | `runtime/uiExtensionHost.ts` | sandbox smoke。 | +| P4-R0 | 计划与事实源收口。 | `docs/roadmap/agentapp/*` | `git diff --check` + legacy 关键词审计。 | +| P4-R1 | v0.3 manifest / fixture / readiness rebaseline。 | `types.ts`、`manifest/*`、`fixtures/*`、`projection/*`、`readiness/*` | parser / projection / readiness / feature flag tests。 | +| P4-R2 | 内容工厂 demo rename 和数据对象 rebaseline。 | `runtime/contentFactoryDemo.ts`、`adapters/*`、`sdk/*` | content factory / adapter / SDK tests。 | +| P4-R3 | WorkflowRuntimeHost rebaseline 到 `content_factory_demo`。 | `runtime/workflowRuntimeHost.ts`、`runtime/runtimePolicy.ts`、`runtime/workflowRuntimeCapabilityProfile.ts` | workflow runtime tests。 | +| P4-R4 | Lab UI、五语言 i18n、index export rebaseline。 | `ui/AgentAppLabPage.tsx`、`index.ts`、`src/i18n/resources/*/agent.json` | UI test + i18n tests。 | +| P4-R5 | Gate 恢复与旧 SceneApp 不复活。 | `src/features/agent-app/*`、相关 dangling 引用 | `test:contracts`、`verify:local`、GUI smoke。 | +| P4.1 | 内容工厂切片。 | `fixtures/*`、`ui/*` | end-to-end demo。 | +| P4.2 | 受控 workflow runtime。 | `runtime/workflowRuntimeHost.ts`、`runtime/runtimePolicy.ts` | workflow runtime tests / UI test。 | +| P5.0 | v0.3 标准差距收口。 | `docs/roadmap/agentapp/p5-cloud-bootstrap.md`、标准 gap matrix | `git diff --check` + legacy 关键词审计。 | +| P5.1 | Cloud Bootstrap payload DTO / parser / validator。 | `install/cloudBootstrap.ts`、`types.ts`、`featureFlag.ts` | payload parser / feature flag tests。 | +| P5.2 | Bootstrap payload 统一 package source adapter。 | `install/*`、`manifest/*`、`projection/*` | package identity / install preview / projection tests。 | +| P5.3 | Tenant enablement + local readiness 合并。 | `readiness/*`、`projection/*`、`ui/*` | readiness / projection / UI tests。 | +| P5.4 | hash mismatch / disable / offline / upgrade 回归。 | `install/*`、`runtime/*`、`ui/*` | uninstall / workflow runtime / UI tests。 | +| P5.5 | Schema / reference CLI cross-check。 | `docs/roadmap/agentapp/*`、上游 `agentapp` reference CLI | `agentapp-ref validate/project/readiness` + 差异记录。 | +| P6 | v0.3 projection / readiness schema coverage。 | `types.ts`、`manifest/*`、`projection/*`、`readiness/*`、`fixtures/*` | parser / projection / readiness / schema coverage tests。 | +| P7 | 本地 schema / snapshot gate。 | `schema/schemaGate.ts`、`schema/schemaGate.test.ts`、`index.ts` | schema gate + projection / readiness tests。 | +| P8 | setup resolver / `needs-setup` 语义。 | `types.ts`、`readiness/*`、`ui/*`、五语言 `agent.json` | readiness / schema gate / UI / i18n tests。 | +| P9 | installed setup state store。 | `install/setupStateStore.ts`、`install/cleanupPlan.ts`、`sdk/*`、`adapters/*`、`ui/*` | setup state / readiness / cleanup / UI tests。 | +| P10 | installed app state snapshot 与 in-memory store。 | `install/installedAppState.ts`、`types.ts`、`install/cleanupPlan.ts`、`ui/*`、`index.ts` | installed state / setup state / projection / UI tests。 | +| P11 | Local Persistence Adapter。 | `install/installedAppState.ts` 或同目录 repository 文件、`install/setupStateStore.ts`、`install/cleanupPlan.ts`、`ui/*` | repository contract / persistence / cleanup / typecheck / contracts。 | +| P12 | Package Cache / Verify / Rollback。 | `install/packageCache.ts`、`install/packageIdentity.ts`、`install/cloudBootstrap.ts`、`install/cleanupPlan.ts`、`readiness/*` | package cache / hash verify / rollback / readiness tests。 | +| P13 | Runtime Package Loader / UI Bundle Loader。 | `install/packageCache.ts`、`runtime/*`、`projection/*`、`ui/*` | loader / UI host / policy guard tests。 | +| P14 | Entry Runtime Guard / Permission Prompt。 | `runtime/*`、`readiness/*`、`install/*`、`ui/*` | guard / permission prompt / setup / policy tests。 | +| P15 | Lab Install / Launch Flow。 | `install/labInstallFlow.ts`、`runtime/entryRuntimeGuard.ts`、`ui/*`、五语言 `agent.json` | lab flow / guard / package cache / installed state / UI tests。 | +| P15-H | GUI smoke / cleanup rehearsal hardening。 | `scripts/agent-app-lab-smoke.mjs`、`featureFlag.ts`、`ui/*`、`docs/roadmap/agentapp/p15-h-gui-smoke-cleanup-rehearsal.md` | `smoke:agent-app-lab` / typecheck / boundary audit。 | +| P16 | Agent App Manager / Product Entry Gate。 | `ui/AgentAppManagerPanel.tsx`、`ui/AgentAppLabPage.tsx`、`install/labInstallFlow.ts`、`runtime/entryRuntimeGuard.ts` | manager UI / entry launcher / lifecycle evidence / GUI smoke。 | +| P16-H | Multi-app repository / lifecycle hardening。 | `install/installedAppState.ts`、`ui/AgentAppManagerPanel.tsx`、`ui/AgentAppManagerPanel.test.tsx`、`scripts/agent-app-lab-smoke.mjs`、`p16-h-multi-app-repository-lifecycle-hardening.md` | repository-backed multi-app list / selected app launcher / persistent lifecycle / cleanup evidence export / residual audit。 | +| P17 Gate | 正式入口前 gate audit。 | `p17-formal-entry-gate-audit.md`、P16-H evidence、boundary audit | gate checklist / command contracts / typecheck / GUI evidence。 | +| P17.0 | Formal Entry Contract。 | `p17-formal-entry-contract.md`、README、implementation plan | `agent-apps` / `agent-app-lab` / runtime surface 职责边界清晰。 | +| P17.1 | Formal route / nav / copy hardening。 | `AgentAppsPage.tsx`、`AppPageContent.tsx`、navigation、五语言 `agent.json` | formal route / copy / sidebar / i18n / boundary tests。 | +| P17.2 | Source / install contract hardening。 | `p17-source-install-contract-hardening.md`、`AgentAppsPage.tsx`、`src/lib/api/agentApps.ts`、`install/*`、上游 `agentapp` schema / reference CLI | P17.2.1-P17.2.5 已覆盖 source state / install review / registration / release descriptor / verification gate / acquisition seam / verified cache source / packageUrl fetch / staging / reference CLI cross-check。 | +| P17.3 | Lifecycle / cleanup contract hardening。 | `p17-lifecycle-cleanup-contract-hardening.md`、`install/*`、`ui/AgentAppsPage.tsx`、cleanup evidence / residual audit、overlay / setup / secret binding 边界 | disable / enable、uninstall keep-data、delete-data rehearsal、export-then-delete preview 都复用同一 namespace 和 evidence,不执行真实删除。 | +| P17.4 | Runtime surface production hardening。 | `ui/AgentAppRuntimePage.tsx`、`runtime/*`、`src/lib/api/agentApps.ts`、`src-tauri/src/commands/agent_app_cmd.rs` | 当前 runtime surface 补丁只算预铺;生产路径必须只加载 verified package cache / staging,不依赖 dev resolver,并证明 injected SDK / sandbox / provenance 没有被绕过。 | +| P17.5 | Formal entry GUI smoke。 | `scripts/agent-apps-smoke.mjs`、正式 `agent-apps` route、P17 evidence summary、`p17-5-formal-entry-gui-smoke.md` | 已完成正式入口 smoke,覆盖 install / registration / launch / disable / uninstall rehearsal / runtime surface / flag-off;Lab smoke 只保留研发回归。 | +| P18 | Typed Capability SDK gate。 | `docs/roadmap/agentapp/p18-typed-capability-sdk-gate.md`、`p18-7-full-lime-capability-surface.md`、`p18-5-content-factory-sdk-regression.md`、`p18-5-3-package-sdk-migration-plan.md`、`p18-completion-audit.md`、`sdk/*`、`runtime/*`、schema / mock / contract tests | 在扩 raw worker 前,把全量 `lime.*` capability catalog、typed host bridge、profile、SDK facade 和 Host discovery 固定为稳定契约,并对齐上游 v0.6 reference sample;P18.7-B v0.6 reference cross-check 已复绿,P18.7-C 再接 `lime.capabilities`。 | ## 验证策略 最低验证层级: -| 改动 | 必跑 | -|---|---| -| 类型 / parser / projection | 定向单测 + `npm run typecheck` 或项目等价命令。 | -| Capability SDK | contract tests。 | -| Tauri command | `npm run test:contracts`。 | -| 用户可见 UI | 相关 `*.test.tsx` + GUI smoke。 | -| Agent App Lab GUI | `npm run smoke:agent-app-lab -- --timeout-ms 180000` + evidence summary。 | -| 主路径接入 | `npm run verify:local` + `npm run verify:gui-smoke`。 | +| 改动 | 必跑 | +| -------------------------- | ------------------------------------------------------------------------- | +| 类型 / parser / projection | 定向单测 + `npm run typecheck` 或项目等价命令。 | +| Capability SDK | contract tests。 | +| Tauri command | `npm run test:contracts`。 | +| 用户可见 UI | 相关 `*.test.tsx` + GUI smoke。 | +| Agent App Lab GUI | `npm run smoke:agent-app-lab -- --timeout-ms 180000` + evidence summary。 | +| 主路径接入 | `npm run verify:local` + `npm run verify:gui-smoke`。 | 进入正式主路径前必须满足: @@ -1180,30 +1180,30 @@ P5 分期以 [p5-cloud-bootstrap.md](./p5-cloud-bootstrap.md) 为准: ## 决策点 -| 决策点 | 时间 | 判断问题 | Go 条件 | No-Go 动作 | -|---|---|---|---|---| -| D0 | P0 结束 | Manifest / projection 是否稳定? | fixture 可稳定 project。 | 停止 runtime 设计,收缩标准。 | -| D1 | P1 结束 | SDK facade 是否足够表达业务? | mock entry 生成可追溯产物。 | 重写 SDK,不接真实 adapter。 | -| D2 | P2 结束 | Adapter 是否足够薄? | 删除 adapter 不影响主路径。 | 回退 mock,重画边界。 | -| D3 | P3 结束 | UI Host 是否安全可控? | App UI 不能越权访问资源。 | 暂停 UI bundle,保留 projection UI。 | -| D4 | P4 结束 | Product-level App 是否有平台价值? | 内容工厂闭环跑通。 | 清理实验岛,不进入正式入口。 | -| D5 | P5 结束 | Cloud bootstrap 是否只是控制面? | 断网已安装 App 可用。 | Cloud 回退为 catalog-only。 | -| D6 | P14 结束 | Entry 授权 gate 是否可靠? | allow / needs-setup / blocked / denied 均有测试,Lab run / mount 不能绕过 guard。 | 暂停 P15,不继续串启动闭环。 | -| D7 | P15 结束 | Lab 安装启动闭环是否可演示且可清理? | install review、permission prompt、launch、cleanup preview 连续可用。 | 保持 Lab-only,不进入正式入口。 | -| D8 | P15-H 结束 | GUI 证据是否足够支撑进入 App 管理面? | Agent App Lab 专用 smoke 通过,summary / 截图 / cleanup preview 可复核。 | 不做 P16 App Manager,先补 GUI 或 cleanup hardening。 | -| D9 | P16 结束 | 最小 Manager 是否足够支撑多 App hardening? | 单 App Manager、entry launcher、disable / uninstall preview、cleanup evidence 与 Agent App Lab smoke 通过。 | 继续补 P16 UI / guard / evidence,不进入多 App。 | -| D10 | P16-H 结束 | 是否可以从 Lab 进入正式 Agent Apps 入口? | 多 App repository、持久化 lifecycle、cleanup evidence 导出、残留检查和 flag-off 回归稳定。 | 继续实验岛,不接正式主导航。 | -| D11 | P17.0 结束 | 正式入口契约是否足够约束后续实现? | `agent-apps` / `agent-app-lab` / runtime surface 职责、禁区、架构和后续 gates 已写清。 | 不进入代码 hardening,先补契约和验收口径。 | -| D12 | P17.1 结束 | 正式入口是否已脱离 Lab 心智? | `agent-apps` route / nav / copy 独立;UI entry 进入 `agent-app` runtime surface;Lab flag-off 不影响正式入口。 | 不进入 source hardening,先修正式入口导航和文案。 | -| D13 | P17.2 结束 | Source / install 契约是否足够支撑真实安装入口? | local / cloud / registration 均有 source state 和 install review;Cloud release 已 pin 到 package / manifest hash;写 repository 前完成 verify;cached fallback 不伪装新安装;上游 schema / reference CLI cross-check 通过;Lab fixture 不作为正式 source。 | 不进入 lifecycle / cleanup hardening,先补 source / install review。 | -| D14 | P17.3 结束 | Lifecycle / cleanup 是否足够支撑可失败退出? | disable / enable、uninstall rehearsal、export-then-delete preview、residual audit、overlay / setup / secret binding 分离均可复核;仍不执行真实 delete-data。 | 不进入 runtime production hardening,先补生命周期和清理证据。 | -| D15 | P17.4 结束 | Runtime surface 是否真正只运行 verified package 且只走标准 Host Bridge? | `cloud_release` 只从 verified cache / staging 启动;dev resolver 仅开发态可用;disabled / unverified package 无法启动;`lime.agentApp.bridge` 覆盖主题、导航、toast、download、capability invoke;SDK bridge / sandbox / provenance 有测试。 | 保持 runtime surface 为预铺,不发布 formal runtime。 | -| D16 | P17.5 结束 | 正式入口 smoke 是否能替代 Lab 证据? | `agent-apps` 专用 smoke 覆盖 install、registration、launch、disable、uninstall rehearsal、runtime surface、flag-off,并输出独立 summary。 | Lab smoke 继续只作研发证据,不把正式入口标为可交付。 | -| D17 | P18 结束 | Typed Capability SDK 是否足够支撑更多 App? | Host Bridge v1、typed SDK facade、stable error、mock host、contract tests、App-scoped Agent task、Lime-side content factory regression、外部 content-factory package verify、dist 同步和 full `verify:local` 均有证据;feature island 仍无 direct invoke / raw Worker 越界。 | 已满足;下一步只做 owner handoff / 提交边界,不进入 raw worker、marketplace 或更多垂直 App。 | +| 决策点 | 时间 | 判断问题 | Go 条件 | No-Go 动作 | +| ------ | ---------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| D0 | P0 结束 | Manifest / projection 是否稳定? | fixture 可稳定 project。 | 停止 runtime 设计,收缩标准。 | +| D1 | P1 结束 | SDK facade 是否足够表达业务? | mock entry 生成可追溯产物。 | 重写 SDK,不接真实 adapter。 | +| D2 | P2 结束 | Adapter 是否足够薄? | 删除 adapter 不影响主路径。 | 回退 mock,重画边界。 | +| D3 | P3 结束 | UI Host 是否安全可控? | App UI 不能越权访问资源。 | 暂停 UI bundle,保留 projection UI。 | +| D4 | P4 结束 | Product-level App 是否有平台价值? | 内容工厂闭环跑通。 | 清理实验岛,不进入正式入口。 | +| D5 | P5 结束 | Cloud bootstrap 是否只是控制面? | 断网已安装 App 可用。 | Cloud 回退为 catalog-only。 | +| D6 | P14 结束 | Entry 授权 gate 是否可靠? | allow / needs-setup / blocked / denied 均有测试,Lab run / mount 不能绕过 guard。 | 暂停 P15,不继续串启动闭环。 | +| D7 | P15 结束 | Lab 安装启动闭环是否可演示且可清理? | install review、permission prompt、launch、cleanup preview 连续可用。 | 保持 Lab-only,不进入正式入口。 | +| D8 | P15-H 结束 | GUI 证据是否足够支撑进入 App 管理面? | Agent App Lab 专用 smoke 通过,summary / 截图 / cleanup preview 可复核。 | 不做 P16 App Manager,先补 GUI 或 cleanup hardening。 | +| D9 | P16 结束 | 最小 Manager 是否足够支撑多 App hardening? | 单 App Manager、entry launcher、disable / uninstall preview、cleanup evidence 与 Agent App Lab smoke 通过。 | 继续补 P16 UI / guard / evidence,不进入多 App。 | +| D10 | P16-H 结束 | 是否可以从 Lab 进入正式 Agent Apps 入口? | 多 App repository、持久化 lifecycle、cleanup evidence 导出、残留检查和 flag-off 回归稳定。 | 继续实验岛,不接正式主导航。 | +| D11 | P17.0 结束 | 正式入口契约是否足够约束后续实现? | `agent-apps` / `agent-app-lab` / runtime surface 职责、禁区、架构和后续 gates 已写清。 | 不进入代码 hardening,先补契约和验收口径。 | +| D12 | P17.1 结束 | 正式入口是否已脱离 Lab 心智? | `agent-apps` route / nav / copy 独立;UI entry 进入 `agent-app` runtime surface;Lab flag-off 不影响正式入口。 | 不进入 source hardening,先修正式入口导航和文案。 | +| D13 | P17.2 结束 | Source / install 契约是否足够支撑真实安装入口? | local / cloud / registration 均有 source state 和 install review;Cloud release 已 pin 到 package / manifest hash;写 repository 前完成 verify;cached fallback 不伪装新安装;上游 schema / reference CLI cross-check 通过;Lab fixture 不作为正式 source。 | 不进入 lifecycle / cleanup hardening,先补 source / install review。 | +| D14 | P17.3 结束 | Lifecycle / cleanup 是否足够支撑可失败退出? | disable / enable、uninstall rehearsal、export-then-delete preview、residual audit、overlay / setup / secret binding 分离均可复核;仍不执行真实 delete-data。 | 不进入 runtime production hardening,先补生命周期和清理证据。 | +| D15 | P17.4 结束 | Runtime surface 是否真正只运行 verified package 且只走标准 Host Bridge? | `cloud_release` 只从 verified cache / staging 启动;dev resolver 仅开发态可用;disabled / unverified package 无法启动;`lime.agentApp.bridge` 覆盖主题、导航、toast、download、capability invoke;SDK bridge / sandbox / provenance 有测试。 | 保持 runtime surface 为预铺,不发布 formal runtime。 | +| D16 | P17.5 结束 | 正式入口 smoke 是否能替代 Lab 证据? | `agent-apps` 专用 smoke 覆盖 install、registration、launch、disable、uninstall rehearsal、runtime surface、flag-off,并输出独立 summary。 | Lab smoke 继续只作研发证据,不把正式入口标为可交付。 | +| D17 | P18 结束 | Typed Capability SDK 是否足够支撑更多 App? | Host Bridge v1、typed SDK facade、stable error、mock host、contract tests、App-scoped Agent task、Lime-side content factory regression、外部 content-factory package verify、dist 同步已有证据;P18.7 已补全全量 capability catalog 并修复上游 v0.6 reference 兼容缺口。 | 未完全满足;P18.7-B 标准 v0.6 兼容已完成,下一步进入 P18.7-C Host capability discovery,不进入 raw worker、marketplace 或更多垂直 App。 | ## 下一刀 -路线图已升级为 Agent App v0.4 Host Bridge 对齐 / 内容工厂,且 P4-R rebaseline、P5.0-P5.5 Cloud Bootstrap、P6 schema coverage、P7 schema gate、P8 setup resolver、P9 setup state store、P10 installed state snapshot、P11 local persistence adapter、P12 package cache / verify / rollback、P13 runtime package loader、P14 entry runtime guard、P15 lab install / launch flow、P15-H Agent App Lab 专用 GUI smoke、P16 Agent App Manager、P16-H multi-app lifecycle hardening、P17 Gate 审计、P17.0、P17.1、P17.2.1-P17.2.5、P17.3、P17.4.1-P17.4.5、P17.5 formal entry GUI smoke、P18.1 Typed SDK facade / mock host / stable error、P18.2 Host Bridge typed router / stable error response、P18.3 Core capability adapters、P18.4 App-scoped Agent task SDK facade、P18.4-H AgentRuntime handoff gate、P18.5.1 Lime-side 内容工厂 SDK regression、P18.5.2 package-side read-only tests、P18.5-S Host Bridge SDK client、P18.5.3 package-side SDK facade / verify / dist 同步与 P18.6 Raw Worker 前 Gate 已通过定向 / 专用验证 / GUI smoke / 计划收口。当前主线下一刀只做 owner handoff / 提交边界;不新增 marketplace、Cloud 管理台、真实 delete-data 或完整行业内容系统。 +路线图已升级为 Agent App v0.4 Host Bridge 对齐 / 内容工厂,且 P4-R rebaseline、P5.0-P5.5 Cloud Bootstrap、P6 schema coverage、P7 schema gate、P8 setup resolver、P9 setup state store、P10 installed state snapshot、P11 local persistence adapter、P12 package cache / verify / rollback、P13 runtime package loader、P14 entry runtime guard、P15 lab install / launch flow、P15-H Agent App Lab 专用 GUI smoke、P16 Agent App Manager、P16-H multi-app lifecycle hardening、P17 Gate 审计、P17.0、P17.1、P17.2.1-P17.2.5、P17.3、P17.4.1-P17.4.5、P17.5 formal entry GUI smoke、P18.1 Typed SDK facade / mock host / stable error、P18.2 Host Bridge typed router / stable error response、P18.3 Core capability adapters、P18.4 App-scoped Agent task SDK facade、P18.4-H AgentRuntime handoff gate、P18.5.1 Lime-side 内容工厂 SDK regression、P18.5.2 package-side read-only tests、P18.5-S Host Bridge SDK client、P18.5.3 package-side SDK facade / verify / dist 同步与 P18.6 Raw Worker 前 Gate 已通过定向 / 专用验证 / GUI smoke / 计划收口。当前主线下一刀是 P18.7-C Host capability discovery;P18.7-B 标准 v0.6 兼容与 reference cross-check 已复绿。不新增 marketplace、Cloud 管理台、真实 delete-data 或完整行业内容系统。 ```text P4-R0~P4-R5 v0.3 / 内容工厂 Rebaseline(已通过) @@ -1253,8 +1253,11 @@ P4-R0~P4-R5 v0.3 / 内容工厂 Rebaseline(已通过) → P18.4-H AgentRuntime handoff gate(已完成) → P18.5.1 Lime-side 内容工厂 SDK regression(已完成) → P18.5.2 Package-side read-only tests(已完成) +→ P18.5.3 Package-side SDK facade / verify(已完成) → P18.6 Raw Worker 前 Gate(已完成) -→ P18.5.3 Package-side SDK facade / verify(下一刀) +→ P18.7-A Full Lime capability catalog / SDK / profile(已完成第一刀) +→ P18.7-B Agent App v0.6 reference compatibility(已完成) +→ P18.7-C Host capability discovery(下一刀) ``` P14 文件边界: diff --git a/docs/roadmap/agentapp/lime-capability-surface.md b/docs/roadmap/agentapp/lime-capability-surface.md new file mode 100644 index 000000000..d1ea7368e --- /dev/null +++ b/docs/roadmap/agentapp/lime-capability-surface.md @@ -0,0 +1,98 @@ +# Lime Agent App 开放能力清单 + +更新时间:2026-05-16 + +## 概述 + +Lime 主 App 通过统一的 `lime.* capability surface` 向 Agent App 开放平台能力。本文档梳理所有开放能力,分门别类列出现状和用途。 + +## 能力分组 + +### 1. 应用表面 (app_surface) + +| 能力 | 版本 | 阶段 | Owner | 方法 | 说明 | +|------|------|------|-------|------|------| +| `lime.ui` | 0.3.0 | current | desktop_host | toast, navigate, openExternal, download, getSnapshot | 桌面壳的提示、导航、下载、主题和快照能力 | +| `lime.events` | 0.3.0 | preview | desktop_host | emit, subscribe, unsubscribe, listSubscriptions | App UI、worker、Host 和 runtime 间的事件通道 | +| `lime.workspace` | 0.3.0 | preview | desktop_host | getCurrent, list, open, getPathRef | 当前 workspace、路径引用、打开入口和工作区上下文 | + +### 2. 数据 (data) + +| 能力 | 版本 | 阶段 | Owner | 方法 | 说明 | +|------|------|------|-------|------|------| +| `lime.storage` | 0.3.0 | current | desktop_host | get, set, list, delete | App namespace 下的结构化业务状态和轻量数据存储 | +| `lime.files` | 0.3.0 | current | desktop_host | pick, readRef, parse | 用户授权文件、file ref 读取和基础解析入口 | +| `lime.knowledge` | 0.3.0 | current | knowledge_runtime | search, bindStatus, bind, export | 项目知识、App knowledge binding、检索和版本导出 | +| `lime.artifacts` | 0.3.0 | current | artifact_runtime | create, open, export, list | 产物创建、打开、导出、列表和 provenance | +| `lime.documents` | 0.3.0 | preview | tool_runtime | parse, export, transform, summarize | PDF、Word、Markdown、PPT 等文档解析、转换和导出 | + +### 3. Agent 运行时 (agent_runtime) + +| 能力 | 版本 | 阶段 | Owner | 方法 | 说明 | +|------|------|------|-------|------|------| +| `lime.agent` | 0.3.0 | current | agent_runtime | startTask, streamTask, getTask, cancelTask, retryTask, submitHostResponse, listTasks | App-scoped Agent task、流式过程、追问、确认、取消和重试 | +| `lime.workflow` | 0.3.0 | current | agent_runtime | start, checkpoint, awaitHuman | App workflow、checkpoint、后台任务和人类确认 | +| `lime.models` | 0.3.0 | preview | agent_runtime | list, select, getRouting, estimateCost | 模型列表、模型路由、能力约束和预估成本 | +| `lime.memory` | 0.3.0 | preview | agent_runtime | query, write, compact, getStatus | 工作记忆、长期记忆、团队记忆和上下文压缩 | +| `lime.skills` | 0.3.0 | preview | agent_runtime | list, resolve, bind, invoke, getInvocation | Skill 注册、发现、绑定、启用状态和调用过程 | +| `lime.context` | 0.3.0 | preview | agent_runtime | getSnapshot, attach, detach | 会话上下文、选中资源、当前任务和可附加上下文 | +| `lime.automation` | 0.3.0 | preview | agent_runtime | startJob, getJob, cancelJob | 自动化 job、周期任务和服务型 Skill 编排 | + +### 4. 治理 (governance) + +| 能力 | 版本 | 阶段 | Owner | 方法 | 说明 | +|------|------|------|-------|------|------| +| `lime.policy` | 0.3.0 | current | policy_runtime | check, requestPermission | 权限、风险、成本、数据和企业策略检查 | +| `lime.secrets` | 0.3.0 | current | policy_runtime | getRef, requestBinding | OAuth、API key、外部平台凭证和 secret ref | +| `lime.capabilities` | 0.3.0 | preview | desktop_host | list, get, getProfile | Host capability catalog、版本、可用性和 readiness 摘要 | +| `lime.settings` | 0.3.0 | preview | desktop_host | get, set, list | App 可见设置、workspace overlay 和 tenant 默认值 | +| `lime.review` | 0.3.0 | preview | policy_runtime | requestDecision, submitDecision, listPending | 人工审核、风险确认、发布门禁和决策记录 | + +### 5. 集成 (integration) + +| 能力 | 版本 | 阶段 | Owner | 方法 | 说明 | +|------|------|------|-------|------|------| +| `lime.tools` | 0.3.0 | current | tool_runtime | invoke, getProgress | Tool Broker / ToolHub 的受控工具调用与长任务状态 | +| `lime.mcp` | 0.3.0 | preview | tool_runtime | listServers, listTools, invoke | MCP server、tool inventory 和受控调用 | +| `lime.browser` | 0.3.0 | preview | tool_runtime | open, navigate, extract, screenshot, close | 浏览器自动化、网页读取、截图和会话隔离 | +| `lime.search` | 0.3.0 | preview | tool_runtime | query, deepResearch, getRun | 网页搜索、深度研究、来源和运行状态 | +| `lime.media` | 0.3.0 | preview | tool_runtime | generateImage, editImage, transcribe, synthesizeVoice | 图片、音频、语音、视频素材的生成和处理 | +| `lime.terminal` | 0.3.0 | preview | tool_runtime | run, getRun, cancel | 命令执行、日志、取消、sandbox 和审批 | +| `lime.connectors` | 0.3.0 | preview | cloud_overlay | list, requestAuth, getStatus, invoke | 外部系统连接器、授权状态和受控集成调用 | + +### 6. 可观测性 (observability) + +| 能力 | 版本 | 阶段 | Owner | 方法 | 说明 | +|------|------|------|-------|------|------| +| `lime.evidence` | 0.3.0 | current | artifact_runtime | record, linkArtifact, list | 来源、引用、工具调用、评估和发布证据 | +| `lime.usage` | 0.3.0 | preview | agent_runtime | getTokenUsage, getCostSummary, getBudget | Token、费用、预算、任务和 App 级用量归因 | +| `lime.tasks` | 0.3.0 | preview | agent_runtime | list, get, cancel, subscribe | 跨 App / runtime 的后台任务、队列、状态和订阅 | + +## 阶段说明 + +- **current**: 已实现,可用于生产 +- **preview**: 部分实现,可用于测试和预览 +- **planned**: 计划中,尚未实现 + +## Owner 说明 + +- **desktop_host**: Lime 桌面应用宿主 +- **agent_runtime**: Agent 运行时 +- **tool_runtime**: 工具运行时 +- **knowledge_runtime**: 知识运行时 +- **artifact_runtime**: 产物运行时 +- **policy_runtime**: 策略运行时 +- **cloud_overlay**: 云覆盖层 + +## 统计 + +- **总能力数**: 27 +- **current 阶段**: 11 +- **preview 阶段**: 16 +- **planned 阶段**: 0 + +## 相关文档 + +- [P18.7 Full Lime Capability Surface](./p18-7-full-lime-capability-surface.md) - 全量能力分层和接线计划 +- [Capability SDK](./capability-sdk.md) - 客户端 Capability SDK 方案 +- [Agent App 路线图](./README.md) - Agent App 整体路线图 diff --git a/docs/roadmap/agentapp/p18-7-full-lime-capability-surface.md b/docs/roadmap/agentapp/p18-7-full-lime-capability-surface.md new file mode 100644 index 000000000..22cfdb262 --- /dev/null +++ b/docs/roadmap/agentapp/p18-7-full-lime-capability-surface.md @@ -0,0 +1,208 @@ +# P18.7 Full Lime Capability Surface + +更新时间:2026-05-16 + +状态:P18.7-A 到 P18.7-E Host first-cut 已落地;标准 v0.6 分层 manifest / `app.runtime.yaml` 兼容、capability discovery、AgentRuntime resource projection、Tool / Integration 受控 intent、内容工厂 Host iframe profile 消费和完整 GUI smoke 已有当前证据。P18.7-F 的最小真实按钮 E2E 已通过:内容工厂 iframe 内“整理知识库”会进入 `lime.agent.startTask`、拿到 Host task id(taskIdSource=hostTaskRunRecord)、启动 models/usage/skills 运行事实拉取和 `lime.agent.streamTask` 订阅;仍需继续推进完成态 artifact / evidence / workspace patch 与逐页面长链路 E2E。 + +## 一句话目标 + +把 Lime 主 App 的所有可复用功能抽象成统一 `lime.* capability surface`,让内容工厂和后续业务 App 在自己的业务工作台内完成完整 AI Agent 流程,同时继续复用 Lime 的 AgentRuntime、Claw 能力、模型路由、Skills、Tools、MCP、浏览器、搜索、媒体、终端、记忆、凭证、策略、Token 用量、Artifact 和 Evidence。 + +这不是给内容工厂补更多专用 API,而是把 Lime 作为 Agent OS 的能力边界固定下来。 + +## 背景判断 + +用户提出的核心问题是:Lime 本身是 Chat UI 形态的 AI Agent 工具,但业务流程很难都塞进 Chat;因此需要在 Lime 之上运行多个业务 App。App 可以很多个,但重点不是“多 App”,而是: + +1. 用户应停留在业务 App 的页面、表单、看板和 workflow 内完成工作。 +2. App 仍必须获得完整 Lime AI Agent 能力,否则独立 Web / 独立 App 更合理。 +3. 完整能力不能等同于模型 API,也不能复制 Claw UI 或 `*_skill_launch` 到每个 App。 +4. Lime 主 App 必须把底层能力封装成稳定 capability,App 只消费这些 capability。 +5. Lime Experts 保持对话优先的专家模块定位;Agent App 可以包含 `expert-chat` 入口,但完整业务流程必须落在可安装业务工作台内。 + +新增硬验收:凡是业务 App 页面触发“整理、生成、分析、推荐、复核、导出结论”任一 AI 动作,页面必须真实进入 Lime AgentRuntime,并展示业务化的 AI 运行现场。运行现场至少包含思考、执行、流式输出、Skill、工具、模型、Token、费用、artifact 和 evidence;完成后可以默认折叠,但过程不能在数据层消失。做不到这一点的页面,不算完整 Agent App,要重新设计或下线该 AI 动作。 + +## 固定边界 + +| 责任 | Lime 主 App / AgentRuntime 做 | 业务 App 做 | 禁止 | +| ---------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------- | ---------------------------------------------- | +| 业务形态 | 提供 UI Host、主题、导航、权限、运行过程投影。 | 定义业务页面、业务对象、流程阶段、人工确认。 | 把所有业务页面塞回通用 Chat。 | +| Agent 执行 | 托管 session/thread/turn/task、队列、模型、上下文、工具、证据。 | 组装 task input、expected output、write-back 目标。 | App 自建第二套 Agent task runtime。 | +| Claw 能力复用 | 把 `@配图 / @搜索 / @研报 / @PDF / @PPT / @浏览器` 等注册成可复用 capability。 | 声明需要的 capability hint 并消费结构化结果。 | 复制 Claw UI、prompt 分支或 Rust launch 实现。 | +| 模型 / Token / 成本 | 统一模型事实源、provider 能力、路由、usage telemetry、预算策略。 | 展示业务任务成本和降级提示。 | App 直接保存 provider key 或自己统计 token。 | +| Skills / Tools / MCP | 管 catalog、workspace binding、runtime gate、Tool Broker、MCP bridge。 | 声明必需 Skill / Tool / MCP capability。 | App 直连 MCP server 或直接运行底层 tool。 | +| 文件 / 搜索 / 浏览器 / 媒体 / 终端 | 管授权、sandbox、执行、审计、artifact/evidence。 | 提供业务 brief、筛选规则、结果落点。 | App 绕过 policy 调文件系统、浏览器、shell。 | +| Storage / Artifact / Evidence | namespace 隔离、持久化、导出、provenance。 | 定义业务 schema、产物类型、证据类型。 | 聊天文本成为唯一结果或证据。 | +| Policy / Secrets / Review | 统一权限、secret ref、企业策略、人工审核。 | 解释用途并处理拒绝 / 重试。 | App 读取 secret 明文或自行做审批绕过。 | + +## 单一事实源 + +| 事实 | current source | 要求 | +| ---------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------ | +| capability 名称、分组、owner、阶段 | `src/features/agent-app/sdk/capabilityCatalog.ts` | 新增 `lime.*` 只能先改 catalog。 | +| typed invoke contract | `src/features/agent-app/sdk/capabilityContract.ts` | 每个 capability 必须有 typed method,未接线也返回 stable error。 | +| SDK facade | `src/features/agent-app/sdk/capabilityAdapters.ts` | `createLimeCoreCapabilityAdapters()` 从 catalog 生成全部 adapter。 | +| readiness profile | `src/features/agent-app/readiness/hostCapabilityProfile.ts` | profile 覆盖全部能力,未实现默认 `none`。 | +| mock / adapter profile | `mockCapabilityProfile.ts`、`adapterCapabilityProfile.ts` | 只从 catalog 派生,不再维护平行数组。 | +| runtime execution | AgentRuntime / ToolRuntime / Desktop Host | 业务 App 不拥有执行事实源。 | +| 产品解释 | 本文、`capability-sdk.md`、`app-surface-runtime.md` | 文档必须先说明边界,再做代码接线。 | + +## 全量能力分层 + +| 层 | Capability | 当前阶段 | 后端 owner | 说明 | +| ---------------- | --------------------------------------------------------------------------------------------------------- | ----------------- | --------------------------------------------- | ----------------------------------------------- | +| Host / UI | `lime.ui`、`lime.events`、`lime.workspace` | current / preview | Desktop Host | 主题、导航、事件、workspace 上下文。 | +| App data | `lime.storage`、`lime.files`、`lime.knowledge`、`lime.artifacts`、`lime.documents` | current / preview | Desktop / Knowledge / Artifact / Tool Runtime | 业务数据、文件、知识、产物、文档解析导出。 | +| Agent runtime | `lime.agent`、`lime.workflow`、`lime.automation`、`lime.tasks` | current / preview | AgentRuntime | App-scoped task、workflow、后台 job、任务中心。 | +| Agent resources | `lime.models`、`lime.usage`、`lime.memory`、`lime.skills`、`lime.context` | preview | AgentRuntime | 模型路由、成本、记忆、Skill、上下文。 | +| Tool integration | `lime.tools`、`lime.mcp`、`lime.browser`、`lime.search`、`lime.media`、`lime.terminal`、`lime.connectors` | current / preview | ToolRuntime / Cloud Overlay | 工具、MCP、浏览器、搜索、媒体、终端、外部连接。 | +| Governance | `lime.policy`、`lime.secrets`、`lime.settings`、`lime.review`、`lime.capabilities` | current / preview | Policy / Desktop Host | 权限、凭证、配置、审核、能力发现。 | +| Evidence | `lime.evidence` | current | Artifact / Evidence Runtime | 来源、评估、发布证据和可审计链路。 | + +## 分期计划 + +### P18.7-A:Catalog / SDK / Profile 收敛 + +状态:第一刀已完成。 + +完成标准: + +1. `LIME_CAPABILITY_NAMES` 从 catalog 派生。 +2. p0 / mock / adapter profile 从 catalog 派生。 +3. SDK adapter 按 catalog 生成全部 facade key。 +4. `capabilityContract.test.ts` 断言 catalog、contract、profile 不漂移。 +5. `capabilityAdapters.test.ts` 断言全部 adapter key 和 method 存在。 +6. `publicSdkSurface.test.ts` 固定 SDK-only public surface。 + +### P18.7-B:文档与标准 v0.6 对齐 + +状态:已完成当前 v0.6 兼容;不把分层 manifest / runtime contract 新字段静默扩成未治理的生产能力。 + +完成标准: + +1. Agent App 文档承认上游 `/Users/coso/Documents/dev/ai/limecloud/agentapp` 已进入 `manifestVersion: 0.6.0`,并新增 `app.runtime.yaml`。 +2. Lime `normalizeManifest` / readiness 明确支持 `0.2 / 0.3 / 0.5 / 0.6`,其他版本继续阻断。 +3. v0.6 兼容不应变成“放宽所有字段”;只允许先做 version normalization、capability list、layered manifest、`agentRuntime` 保留和 reference cross-check 复绿。 +4. `docs/roadmap/agentapp/README.md`、`capability-sdk.md`、`app-surface-runtime.md` 必须回链本文。 +5. `referenceCliCrossCheck.test.ts` 必须兼容 reference CLI 的 capability list 和 `agentRuntime`,并显式登记 discovery / marketplace / compliance / health / runtime policy 等尚未投影字段的退出条件。 + +### P18.7-C:Host Capability Profile / Discovery Surface + +状态:first-cut 已落地;Host discovery 和业务 App profile 消费已具备最小路径,仍需补齐更多后端 owner / unavailable reason。 + +目标:让 App 能通过 `lime.capabilities.getProfile / list / get` 看到 Host 当前能力、实现方式、stage、owner 和 unavailable 原因。 + +完成标准: + +1. Host snapshot / Host Bridge 可投影 catalog 摘要。 +2. `lime.capabilities` 不直接暴露 Lime internal path。 +3. preview 能力未接线时明确 `enabled=false / implementation=none`。 +4. App UI 根据 profile 降级,不自行猜测底层能力。 + +### P18.7-D:AgentRuntime Resource 能力接线 + +状态:first-cut 已落地;`lime.models / lime.usage / lime.skills` 可进入 Host runtime facts,深水位模型约束、预算策略和 workspace skill binding 仍需补齐。 + +优先级: + +1. `lime.usage`:从 request telemetry / runtime facts 投影 model、token、cost、budget。 +2. `lime.models`:读取模型事实源、路由结果和 capability constraints。 +3. `lime.skills`:读取 workspace skill binding、ready 状态、调用证据。 +4. `lime.memory` / `lime.context`:读取 session/thread/turn context 与记忆状态,不自动写入。 + +完成标准: + +- 内容工厂的运行过程面板可以展示模型、Token、费用、Skill 使用、上下文状态,且这些事实来自 Host / AgentRuntime projection,不来自 App 自己解析底层事件。 + +### P18.7-E:Tool / Integration 能力接线 + +状态:Host first-cut 已落地;执行型能力只返回受控 `requires_agent_task` intent 或只读投影,真实 ToolRuntime / Connector execution gate 仍需后续深水位。 + +优先级: + +1. `lime.search` / `lime.browser`:支撑内容工厂资料补齐、竞品调研、网页来源。 +2. `lime.documents` / `lime.media`:支撑 PDF/Word/PPT 解析导出、图片/音频/视频素材。 +3. `lime.mcp` / `lime.terminal`:支撑高级工具场景,但必须经过 policy/sandbox。 +4. `lime.connectors`:后移到 tenant / secret / Cloud overlay 稳定后。 + +完成标准: + +- App 只能声明 capability intent;实际工具、浏览器、MCP、终端执行继续由 Lime 主 App 管理权限、进度和 evidence。 + +### P18.7-F:内容工厂产品闭环复核 + +状态:first-cut 已完成;知识库整理、场景、内容战役、脚本、交付、复盘均已具备 typed Agent task / Host Bridge 主路径和页面内运行现场,外部内容工厂测试已覆盖 Host profile / runtime facts / workspace patch 写回,Lime Host focused smoke 已证明 iframe 内 Host profile 可见。可选深水位 smoke 已证明“整理知识库”真实按钮能进入 `lime.agent.startTask`、拿到 Host task id(taskIdSource=hostTaskRunRecord)、启动 models/usage/skills 运行事实拉取和 `lime.agent.streamTask` 订阅;仍需 Host iframe 内逐个真实业务 AI 动作完成态 E2E 证明。 + +完成标准: + +1. 内容工厂“整理知识库 / 生成场景 / 生成文案和配套素材 / 只重写文案 / 生成脚本 / 交付包 / 复盘”真实通过 `lime.agent` 进入 AgentRuntime。 +2. 运行过程展示来自 Host `runtimeProcess`,完成后折叠但不消失。 +3. 使用的模型、Token、费用、Skill、工具、引用、artifact、evidence 均可见。 +4. 最终业务结果写回 App storage / artifacts / evidence。 +5. 不需要跳回 Lime 通用 Chat,也不直接调用模型 API。 +6. standalone 页面只可作为 UI smoke,不可替代 Host iframe 内真实 AgentRuntime 闭环证据。 + +## v0.6 标准兼容策略 + +原阻塞事实:外部标准示例 `/Users/coso/Documents/dev/ai/limecloud/agentapp/docs/examples/content-factory-app/APP.md` 已进入 `manifestVersion: 0.6.0`,且 v0.6 通过 `app.runtime.yaml` 声明 Agent task event/result、structured output、approval、session policy、tool discovery、checkpoint 和 observability contract;旧版 Lime 只识别 `APP.md` frontmatter,会漏掉分层文件。当前已完成版本无关 layered manifest resolver 与前端 merge,阻塞解除。 + +兼容策略: + +1. 把 `0.6.x` normalize 成内部 `"0.6"`,继续保留 `0.5` compat,不静默降级为 `0.3`。 +2. `supportsManifestRuntime()` 明确支持 `0.6`,并保留 unsupported blocker 给其他版本。 +3. `supportsRequestedRange()` 支持 `@lime/app-sdk@^0.6.0` 和 capability list `lime.*` 的当前 profile 匹配。 +4. reference cross-check 的 `manifestVersion` 期望应从 reference projection 读取,不再硬编码 `0.3`。 +5. v0.6 新字段若尚未进入 Lime projection / runtime policy,必须在 accepted divergences 写清楚退出条件;不能无声丢字段。 + +落地记录: + +- `normalizeManifest()` 将 `0.6.x` 归一化为内部 `"0.6"`,保留 `0.2 / 0.3 / 0.5`,其他版本继续报错。 +- `checkReadiness()` 将 `"0.6"` 视为受支持 manifest runtime,并可解析 `@lime/app-sdk@^0.6.0` 形式的 SDK range。 +- `referenceCliCrossCheck.test.ts` 兼容 reference CLI `capabilityRequirements.capabilities` 从 object 到 array 的变化;当 reference readiness 不再输出 capability check 时,以 projection capability list 为对齐事实。 +- `parseManifest.test.ts` 增加 v0.6 capability list 和 layered manifest 回归,确认 `lime.agent / lime.skills / lime.usage` 被归一化为 `"*"` range,且 `app.runtime.yaml` 能合并为 `agentRuntime`。 +- v0.5/v0.6 新增 metadata / discovery / runtime policy 字段未完全进入 Lime projection 的部分,已在 accepted divergences 逐项登记;退出条件是 P18.7-C Host discovery、Agent App runtime policy 和后续 marketplace / cloud release review 正式投影。 + +## 后端接线顺序 + +| 顺序 | 先接能力 | 原因 | 不做 | +| ---- | ----------------------------------------------- | --------------------------------------------- | ------------------------------- | +| 1 | `lime.capabilities` | 让 App 先知道 Host 到底支持什么。 | 不把 catalog owner 下放给 App。 | +| 2 | `lime.usage` / `lime.models` | 用户明确要求模型、Token、费用可见。 | 不在 App 内估算 token。 | +| 3 | `lime.skills` | 内容生成必须用 Skills。 | 不复制 Claw skill launch。 | +| 4 | `lime.search` / `lime.documents` / `lime.media` | 内容工厂最直接需要资料、文档、图片/素材能力。 | 不做内容工厂专用 command。 | +| 5 | `lime.browser` / `lime.mcp` / `lime.terminal` | 高风险工具先受控接入。 | 不绕过 sandbox / policy。 | +| 6 | `lime.memory` / `lime.context` / `lime.tasks` | 提升长期业务连续性和任务中心统一。 | 不做第二套任务队列。 | + +## 验收清单 + +| 要求 | 证据 | +| ----------------------------- | --------------------------------------------------------------------------------------------------------- | +| 全量能力名只有一份事实源。 | catalog 测试断言 `LIME_CAPABILITY_NAMES`、profile keys、adapter keys 一致。 | +| App 不再自建底层 Agent 能力。 | 内容工厂只调用 SDK facade;无裸模型 API / 私有 bridge / 专用 Tauri command。 | +| Lime 主 App 封装运行过程。 | `runtimeProcess` 包含 thinking、text、execution、tools、skills、model、usage、cost、artifacts、evidence。 | +| 过程不消失。 | 终态默认折叠但 timeline 保留。 | +| 使用 Skills。 | `lime.agent` task 的 capability hints / skill bindings 可被 Host 投影,调用证据可见。 | +| 模型和 Token 可见。 | `lime.usage` / `runtimeProcess.usage` 来自 AgentRuntime telemetry。 | +| 页面确实 Agent 化。 | 知识库、场景、战役、交付、复盘页面的 AI 动作均构造 typed Agent task;Host connected 时本地生成 API 被硬拒绝。 | +| v0.6 标准兼容。 | `referenceCliCrossCheck.test.ts` 重新通过,不靠跳过;Host / 前端 resolver 能读 `app.runtime.yaml`。 | +| GUI 主路径可交付。 | 定向测试、`typecheck`、`test:contracts`、必要时 `verify:gui-smoke`。 | + +## current / compat / deprecated / dead + +- `current`:`lime.* capability catalog + typed SDK + Host Bridge + AgentRuntime facts`。 +- `compat`:现有 mock / adapter / workflow host,可服务 preview 和测试,但只能从 catalog 派生。 +- `deprecated`:App 手写 bridge、App 自己解析 runtime 底层事件、各文件自建 capability 数组。 +- `dead`:App 裸调用模型 API、复制 Claw UI/skill launch、垂直专用 `content_factory_*` 后端命令。 + +## 当前验证证据 + +- Lime Host:2026-05-16 23:05 `npm run verify:gui-smoke` 通过,覆盖 workspace ready、browser runtime、site adapters、Skill Forge entry、runtime tool surface、runtime surface page、`@` command registry、Agent Apps、Claw streaming、Knowledge GUI、Design Canvas。 +- Agent Apps:2026-05-16 23:08 增强后的 `npm run smoke:agent-apps -- --timeout-ms 180000 --prefix agent-apps-smoke-p18-7-runtime-frame-profile` 通过,`runtimeFrameContentFactoryLoaded=true` 且 `runtimeFrameHostProfileVisible=true`。 +- Agent Apps deep gate:2026-05-16 23:48 `npm run smoke:agent-apps -- --timeout-ms 180000 --prefix agent-apps-smoke-p18-7-content-action-e2e-fixed5 --include-content-factory-action-e2e` 通过;点击内容工厂 iframe 内“知识库底座 -> 整理知识库”后,summary 断言 `contentFactoryActionStarted=true`、`contentFactoryActionTaskAccepted=true`、`contentFactoryActionRuntimeFactsObserved=true`、`contentFactoryActionRuntimeFactsStarted=true`、`contentFactoryActionStreamOrGetTaskStarted=true`、`contentFactoryActionRequiredSkillsProjected=true`、`contentFactoryActionNoHostFallback=true`。当前 task-scoped record 仍显示 `modelLabel=模型等待路由`、`hasUsage=false`、`hasCost=false`、`invokedSkillNames=[]`、`artifactCount=0`,所以完成态仍未覆盖。 +- Agent Apps completion gate:2026-05-16 23:55 新增并运行 `npm run smoke:agent-apps -- --timeout-ms 180000 --prefix agent-apps-smoke-p18-7-completion-gate-current --include-content-factory-completion-e2e --completion-timeout-ms 30000`,当前预期失败;failure JSON 明确缺 `modelReady / usageReady / costReady / skillInvocationReady / artifactReady / evidenceReady`,task record 仍是 `active_turn_id=null / profile_status=idle`。 +- Runtime root probe:2026-05-17 00:02 针对同一 session 直接查 `agent_runtime_get_thread_read` / `agent_runtime_get_session`,均为 `idle / queued_turns=0 / turns=0 / messages=0`;SQLite 只有 `agent_sessions` 行,无 `agent_thread_turns`、`agent_messages`、`agent_runs`;`agent_runtime_promote_queued_turn(agent-app-queued-{taskId})` 返回 `false`。下一刀应查 `agent_app_runtime_start_task -> submit_runtime_turn -> spawn_runtime_turn_task` 为什么 accepted 后未产生 runtime turn。 +- 内容工厂:`/Users/coso/Documents/dev/ai/limecloud/content-factory-app` 中 `npm test` 通过 56 项,`npm run validate:app` 返回 `ok=true / status=passed`;`npm run readiness:app` 返回 `ok=true / status=needs-setup`,剩余 warning 是 Host 运行前必须满足的 skills / knowledge / tool / artifact / eval / service 绑定。 + +## 下一刀 + +进入 P18.7-F 深水位:从已通过的最小真实按钮 E2E 继续往后推进,补 runtimeProcess 中模型 / Token / 费用 / Skill invocation 的真实回写断言,再推进 artifact / evidence / workspace patch 完成态;随后按“生成场景 -> 生成内容 -> 交付 / 复盘”的顺序扩展可选 gate。这些完成前,不把“全部业务 AI 动作均真实闭环”标为完成。 diff --git a/docs/roadmap/agentapp/p18-7-parallel-validation.md b/docs/roadmap/agentapp/p18-7-parallel-validation.md new file mode 100644 index 000000000..adb5db9ba --- /dev/null +++ b/docs/roadmap/agentapp/p18-7-parallel-validation.md @@ -0,0 +1,161 @@ +# Agent App P18.7 并行验证记录 + +更新时间:2026-05-16 + +## 主目标 + +推进 `docs/roadmap/agentapp/p18-7-full-lime-capability-surface.md` 中的 P18.7:先完成 P18.7-B 的 Agent App `manifestVersion: 0.6.0` 标准兼容、layered manifest 和 reference cross-check 复绿,再完成 P18.7-C `lime.capabilities` Host discovery surface、P18.7-D AgentRuntime resource projection,并继续进入 P18.7-E Tool / Integration 受控 intent 与只读运行投影。 + +## 并行写集 + +| 归属 | 写集 | 当前处理 | +| --- | --- | --- | +| 隔壁进程 | `src-tauri/**`、外部 `content-factory-app`、未确认摘要 `docs/roadmap/agentapp/lime-capability-surface.md` | 只读审阅和验证;不接管外部 App / Rust / default skills 并行写集,不合并未确认摘要。 | +| 前序本进程 | `AGENTS.md`、`docs/aiprompts/README.md`、`docs/aiprompts/parallel-agent-collaboration.md`、`src/features/agent-app/**`、本文档 | 已记录并行协作规则、P18.7-B/C/D/E first-cut 和 GUI 主路径验证证据。 | +| 2026-05-16 23:35 follow-up | `scripts/agent-apps-smoke.mjs`、`docs/roadmap/agentapp/p18-7-parallel-validation.md`、`docs/roadmap/agentapp/p18-7-full-lime-capability-surface.md` | 只修可选内容工厂真实按钮 E2E gate 的等待 / 断言与对应证据记录。 | + +## 已验证 + +| 命令 | 结果 | 说明 | +| --- | --- | --- | +| `git diff --check` / `git diff --no-index --check`(scoped) | passed | 当前本进程写集含未跟踪 `capabilityCatalog.ts` / `p18-7-parallel-validation.md` 无空白 diff 问题。 | +| `npm test -- "src/features/agent-app/manifest/parseManifest.test.ts" "src/features/agent-app/schema/referenceCliCrossCheck.test.ts"` | passed | 覆盖 v0.6 manifest normalization、layered manifest、reference projection/readiness cross-check。 | +| `npm test -- "src/features/agent-app/readiness/checkReadiness.test.ts" "src/features/agent-app/sdk/capabilityContract.test.ts" "src/features/agent-app/sdk/capabilityAdapters.test.ts" "src/features/agent-app/sdk/publicSdkSurface.test.ts"` | passed | 覆盖 readiness、capability catalog、typed SDK adapter 和公开 SDK surface。 | +| `npm test -- "src/features/agent-app"` | passed | 36 files / 182 tests passed,覆盖 Agent App 定向套件。 | +| `npm run typecheck` | passed | 前端 TypeScript 边界通过。 | +| `npm run test:contracts` | passed | 命令契约、Harness 契约、modality runtime contract、cleanup report contract 均通过。 | +| `npm run governance:legacy-report` | passed | 边界违规 0;报告仍有既有分类漂移候选,但不阻塞本轮 P18.7-B。 | +| `npm run verify:gui-smoke` | passed | 2026-05-16 19:55 在 `AgentAppRuntimePage.tsx` 接入 `lime.capabilities` 后重跑完成;Headless Tauri、DevBridge、workspace ready、browser runtime、site adapters、Agent Apps、Claw streaming、Knowledge GUI、Design Canvas 等最小 GUI smoke 通过。 | +| `npm test -- "src/features/agent-app/runtime/capabilityDispatcher.test.ts" "src/features/agent-app/runtime/hostBridge.test.ts" "src/features/agent-app/sdk/capabilityAdapters.test.ts"` | passed | 覆盖 P18.7-C `lime.capabilities.list/get/getProfile` Host discovery、Host Bridge 分发和 SDK adapter。 | +| `npm test -- "src/features/agent-app/ui/AgentAppRuntimePage.test.tsx" "src/features/agent-app/runtime/capabilityDispatcher.test.ts"` | passed | 2 files / 16 tests passed;覆盖 Agent App Runtime Page 在 snapshot 中声明 `lime.capabilities`,并允许 iframe 通过 `lime.capabilities.getProfile` 读取 Host capability profile。 | +| `npm test -- "src/features/agent-app"` | passed | 36 files / 184 tests passed,新增 Runtime Page `lime.capabilities.getProfile` 回归后 Agent App 定向套件通过。 | +| `npm run typecheck` | passed | P18.7-C discovery 类型边界通过。 | +| `npm test -- "src/features/agent-app/runtime/capabilityDispatcher.test.ts" "src/features/agent-app/ui/AgentAppRuntimePage.test.tsx" "src/features/agent-app/sdk/capabilityContract.test.ts" "src/features/agent-app/sdk/capabilityAdapters.test.ts"` | passed | 4 files / 26 tests passed;覆盖 `lime.models.list/getRouting`、`lime.usage.getTokenUsage/getCostSummary`、manifest 声明边界、catalog/profile/adapter 同步。 | +| `npm test -- "src/features/agent-app"` | passed | 36 files / 186 tests passed,新增 P18.7-D runtime resource projection 回归后 Agent App 定向套件通过。 | +| `npm run typecheck` | passed | P18.7-D `lime.models` / `lime.usage` runtime projection 类型边界通过。 | +| `npm run test:contracts` | passed | 命令契约、Harness 契约、modality runtime contract、cleanup report contract 复核通过;本轮未新增 Tauri 命令。 | +| `npm run verify:gui-smoke` | passed | 2026-05-16 20:23 在 `lime.models` / `lime.usage` Host Bridge 接入后重跑完成;Headless Tauri、DevBridge、workspace ready、browser runtime、site adapters、Agent Apps、Claw streaming、Knowledge GUI、Design Canvas 等最小 GUI smoke 通过。 | +| `npm test -- "src/features/agent-app/runtime/capabilityDispatcher.test.ts" "src/features/agent-app/ui/AgentAppRuntimePage.test.tsx" "src/features/agent-app/sdk/capabilityContract.test.ts" "src/features/agent-app/sdk/capabilityAdapters.test.ts"` | passed | 4 files / 27 tests passed;覆盖 `lime.skills.list/resolve/getInvocation` 从 `runtimeProcess.skillNames/invokedSkillNames` 投影,并确认 `bind` 不伪造成功。 | +| `npm test -- "src/features/agent-app"` | passed | 36 files / 187 tests passed,新增 P18.7-D `lime.skills` runtime projection 回归后 Agent App 定向套件通过。 | +| `npm run typecheck` | passed | P18.7-D `lime.skills` runtime projection 类型边界通过。 | +| `npm run test:contracts` | passed | 命令契约、Harness 契约、modality runtime contract、cleanup report contract 复核通过;本轮仍未新增 Tauri 命令。 | +| `npm run verify:gui-smoke` | passed | 2026-05-16 20:45 在 `lime.skills` Host Bridge 接入后重跑完成;Headless Tauri、DevBridge、workspace ready、browser runtime、site adapters、Agent Apps、Claw streaming、Knowledge GUI、Design Canvas 等最小 GUI smoke 通过。 | +| `npm test -- "src/features/agent-app/runtime/capabilityDispatcher.test.ts" "src/features/agent-app/ui/AgentAppRuntimePage.test.tsx" "src/features/agent-app/sdk/capabilityContract.test.ts" "src/features/agent-app/sdk/capabilityAdapters.test.ts"` | passed | 4 files / 28 tests passed;覆盖 `lime.memory.getStatus/query`、`lime.context.getSnapshot` 的只读 projection,并确认 `write/compact/attach/detach` 不伪造成功。 | +| `npm test -- "src/features/agent-app"` | passed | 36 files / 188 tests passed,新增 P18.7-D `lime.memory` / `lime.context` runtime projection 回归后 Agent App 定向套件通过。 | +| `npm run typecheck` | passed | P18.7-D `lime.memory` / `lime.context` runtime projection 类型边界通过。 | +| `npm run test:contracts` | passed | 命令契约、Harness 契约、modality runtime contract、cleanup report contract 复核通过;本轮仍未新增 Tauri 命令。 | +| `npm run verify:gui-smoke` | passed | 2026-05-16 21:01 在 `lime.memory` / `lime.context` Host Bridge 接入后重跑完成;Headless Tauri、DevBridge、workspace ready、browser runtime、site adapters、Agent Apps、Claw streaming、Knowledge GUI、Design Canvas 等最小 GUI smoke 通过。 | +| `npm test -- "src/features/agent-app/runtime/capabilityDispatcher.test.ts" "src/features/agent-app/ui/AgentAppRuntimePage.test.tsx" "src/features/agent-app/sdk/capabilityContract.test.ts" "src/features/agent-app/sdk/capabilityAdapters.test.ts"` | passed | 4 files / 29 tests passed;覆盖 P18.7-E `lime.search.query/getRun`、`lime.browser.open`、`lime.documents.parse`、`lime.media.generateImage` 的 Host 侧受控 intent 与 runtime tool projection。 | +| `npm test -- "src/features/agent-app"` | passed | 36 files / 189 tests passed,新增 P18.7-E search/browser/documents first-cut 回归后 Agent App 定向套件通过。 | +| `npm run typecheck` | passed | P18.7-E `lime.search` / `lime.browser` / `lime.documents` Host Bridge 类型边界通过。 | +| `npm run test:contracts` | passed | 命令契约、Harness 契约、modality runtime contract、cleanup report contract 复核通过;本轮未新增 Tauri 命令。 | +| `npm run verify:gui-smoke` | passed | 2026-05-16 21:26 在 `lime.search` / `lime.browser` / `lime.documents` Host Bridge 接入后重跑完成;Headless Tauri、DevBridge、workspace ready、browser runtime、site adapters、Agent Apps、Claw streaming、Knowledge GUI、Design Canvas 等最小 GUI smoke 通过。 | +| `npm test -- "src/features/agent-app/runtime/capabilityDispatcher.test.ts" "src/features/agent-app/ui/AgentAppRuntimePage.test.tsx" "src/features/agent-app/sdk/capabilityContract.test.ts" "src/features/agent-app/sdk/capabilityAdapters.test.ts"` | passed | 4 files / 29 tests passed;复核 P18.7-E `lime.media` first-cut:`generateImage` 返回受控 `requires_agent_task` intent,profile / Runtime Page 均暴露 `lime.media` adapter。 | +| `npm test -- "src/features/agent-app"` | passed | 36 files / 189 tests passed,新增 P18.7-E `lime.media` Host first-cut 后 Agent App 定向套件通过。 | +| `npm run typecheck` | passed | P18.7-E `lime.media` Host Bridge 类型边界通过。 | +| `npm run test:contracts` | passed | 命令契约、Harness 契约、modality runtime contract、cleanup report contract 复核通过;本轮未新增 Tauri 命令。 | +| `npm test -- "src/features/agent-app/runtime/capabilityDispatcher.test.ts" "src/features/agent-app/ui/AgentAppRuntimePage.test.tsx" "src/features/agent-app/sdk/capabilityContract.test.ts" "src/features/agent-app/sdk/capabilityAdapters.test.ts"` | passed | 4 files / 29 tests passed;覆盖 P18.7-E `lime.mcp.listServers/invoke`、`lime.terminal.run/getRun/cancel`,确认二者只返回受控 intent / 只读投影 / not_available cancellation。 | +| `npm test -- "src/features/agent-app"` | passed | 37 files / 193 tests passed;在隔壁新增 `agentRuntimeProcess.test.ts` 与 v0.6 manifest 测试后,Agent App 定向套件整体通过。 | +| `npm run typecheck` | passed | P18.7-E `lime.mcp` / `lime.terminal` Host Bridge 类型边界通过。 | +| `npm run test:contracts` | passed | 命令契约、Harness 契约、modality runtime contract、cleanup report contract 复核通过;本轮仍未新增 Tauri 命令。 | +| `npm test -- "src/features/agent-app/runtime/capabilityDispatcher.test.ts" "src/features/agent-app/ui/AgentAppRuntimePage.test.tsx" "src/features/agent-app/sdk/capabilityContract.test.ts" "src/features/agent-app/sdk/capabilityAdapters.test.ts"` | passed | 4 files / 29 tests passed;覆盖 P18.7-E `lime.connectors.list/getStatus/requestAuth/invoke`,确认外部连接器只返回只读投影、Host 授权需求或受控 intent。 | +| `npm test -- "src/features/agent-app"` | passed | 37 files / 193 tests passed;新增 `lime.connectors` Host first-cut 后 Agent App 定向套件整体通过。 | +| `npm run lint` | passed | ESLint `src --max-warnings 0` 通过。 | +| `npm run typecheck` | passed | P18.7-E `lime.connectors` Host Bridge 类型边界通过。 | +| `npm run test:contracts` | passed | 命令契约、Harness 契约、modality runtime contract、cleanup report contract 复核通过;本轮仍未新增 Tauri 命令。 | +| `node --check "scripts/agent-apps-smoke.mjs"` | passed | `smoke:agent-apps` failure diagnostics 脚本语法通过;后续失败会写 `*-failure.json` / `*-failure.png`,便于定位 Runtime 打开链路。 | +| `npm run smoke:agent-apps -- --app-url http://127.0.0.1:9/ --health-url http://127.0.0.1:3030/health --timeout-ms 1000 --interval-ms 100 --prefix agent-apps-smoke-diagnostics-selftest` | expected failure with diagnostics | 使用无效 app URL 触发早期失败,确认已落 `.lime/qc/gui-evidence/agent-apps/agent-apps-smoke-diagnostics-selftest-failure.json` 与 `*-failure.png`;JSON 覆盖 pageState、bridgeHealth、runtimeStatus、consoleErrors、failedRequests。 | +| `npm run smoke:agent-apps -- --app-url http://127.0.0.1:9/ --health-url http://127.0.0.1:3039/health --timeout-ms 1000 --interval-ms 100 --prefix agent-apps-smoke-diagnostics-process-selftest` | expected failure with diagnostics | 使用本命令内临时 fake DevBridge 和无效 app URL 触发早期失败,不触碰外部 `content-factory-app` dev server;确认 failure JSON 新增 `processSnapshot`,包含平台、进程总数、cwd 探测数量、匹配进程和 match reason。 | +| `npm run bridge:health -- --timeout-ms 15000` | passed | 2026-05-16 23:00 DevBridge 恢复 ready,`http://127.0.0.1:3030/health` 返回 `status=ok`。 | +| `npm run smoke:agent-apps -- --timeout-ms 180000 --prefix agent-apps-smoke-p18-7-refocus` | passed | focused Agent Apps smoke 通过;`agent_app_start_ui_runtime` 可打开 Runtime surface,summary 证明 install / disable-enable / launch / uninstall dry-run / flag-off regression 均为 true。 | +| `npm run verify:gui-smoke` | passed | 2026-05-16 23:05 完整 GUI smoke 通过;覆盖 workspace ready、browser runtime、site adapters、Skill Forge entry、runtime tool surface、runtime surface page、`@` command registry、Agent Apps、Claw streaming、Knowledge GUI、Design Canvas。 | +| `npm run smoke:agent-apps -- --timeout-ms 180000 --prefix agent-apps-smoke-p18-7-runtime-frame-profile` | passed | 增强后的 Agent Apps smoke 通过;除 Runtime frame 可见外,还断言 iframe 内内容工厂已加载并展示 Host capability profile/运行事实提示,summary 中 `runtimeFrameContentFactoryLoaded=true`、`runtimeFrameHostProfileVisible=true`。 | +| `npm run smoke:agent-apps -- --timeout-ms 180000 --prefix agent-apps-smoke-p18-7-content-action-e2e --include-content-factory-action-e2e` | failed at deep gate | 新增可选深水位 E2E:iframe 内点击“知识库底座 -> 整理知识库”,已观察到 App 发出 `lime.agent.startTask` 并展示运行现场;failure JSON 后续复盘证明 task id 已出现在 SDK call log,但 host task record 顶层未携带 task id。 | +| `node --check "scripts/agent-apps-smoke.mjs"` | passed | 修正可选 gate 等待逻辑后,脚本语法通过。 | +| `npm run smoke:agent-apps -- --timeout-ms 180000 --prefix agent-apps-smoke-p18-7-content-action-e2e-fixed5 --include-content-factory-action-e2e` | passed | 最小真实按钮 E2E 通过;iframe 内“知识库底座 -> 整理知识库”发出 `lime.agent.startTask`,从 task-scoped host run record 确认 Host task id / runtimeFacts container / required Skills 投影,且已触发 `lime.models.getRouting`、`lime.usage.getTokenUsage/getCostSummary`、`lime.skills.list`、`lime.agent.streamTask`,无 Host fallback。 | +| `npm run smoke:agent-apps -- --timeout-ms 180000 --prefix agent-apps-smoke-p18-7-completion-gate-current --include-content-factory-completion-e2e --completion-timeout-ms 30000` | expected failure | 新增完成态可选 gate;30s 内未达到完成态,failure JSON 显示 `modelReady=false / usageReady=false / costReady=false / skillInvocationReady=false / artifactReady=false / evidenceReady=false`,但 `workspacePatchReady=true`。 | +| `agent_runtime_get_thread_read` / `agent_runtime_get_session` for `agent-app-runtime-1afdd73f-7bb9-4bfb-9204-3268fa08930d` | diagnostic | DevBridge 返回 idle、queued_turns=0、turns=0、messages=0;证明 completion gate 失败不是页面单纯没等够。 | +| `git diff --check -- "scripts/agent-apps-smoke.mjs"` | passed | 本轮脚本补丁无空白 diff 问题。 | +| `npm run smoke:agent-apps -- --timeout-ms 180000 --prefix agent-apps-smoke-p18-7-default-after-action-gate-fixed4` | passed | 复核默认 Agent Apps smoke 未被可选深水位 gate 影响;默认 install / launch / iframe profile / uninstall dry-run / flag-off 仍通过。 | +| `/Users/coso/Documents/dev/ai/limecloud/content-factory-app`: `npm test` | passed | 外部内容工厂 56 tests passed;覆盖 Host Bridge 调用 `lime.capabilities.getProfile`、`lime.models.getRouting`、`lime.usage.getTokenUsage/getCostSummary`、`lime.skills.list`,以及主生产按钮 Host connected 时只走 `lime.agent.startTask`、运行过程/Skill/Token/费用展示和 workspace patch 写回。 | +| `/Users/coso/Documents/dev/ai/limecloud/content-factory-app`: `npm run validate:app` | passed | reference CLI validate 返回 `ok=true / status=passed`,manifest hash `sha256:22d23772240c038ffe27b58ee3da298fac412d2e6b82f0a9c6659d98fecad9d1`。 | +| `/Users/coso/Documents/dev/ai/limecloud/content-factory-app`: `npm run readiness:app` | expected needs-setup | reference CLI readiness 返回 `ok=true / status=needs-setup`;剩余 warnings 是 host 运行前必须满足的 skills / knowledge / tool / artifact / eval / service 绑定,不是 manifest schema failure。 | + +## 验证备注 + +- 2026-05-16 21:50 前后,旧 `npm run verify:local` 进程停在 `verify:gui-smoke` 的 `smoke:agent-apps` 子流程,未产生新的 GUI evidence;为避免占用并行 GUI / DevBridge 环境,已终止该验证链,不计为通过证据。 +- 2026-05-16 21:57 重新执行 `npm run verify:gui-smoke`,workspace / browser runtime / site adapters / agent-service-skill-entry / runtime tool surface / `@` command registry 已通过;随后 `smoke:agent-apps` 在打开 Agent App Runtime 时触发 `agent_app_start_ui_runtime` DevBridge `/invoke` 5s timeout。该命令面位于隔壁持有的 `src-tauri/**` 写集,本进程不接管;本轮 GUI smoke 因并行 Tauri runtime 命令阻塞未计入通过证据。 +- 2026-05-16 22:30 前后,focused `npm run smoke:agent-apps -- --timeout-ms 180000 --prefix agent-apps-smoke-p18-7-refocus` 复测仍停在 `stage=launch-runtime-surface`;复测期间 `http://127.0.0.1:3030/health` 曾从最初 ready 变为 connection refused,随后 `npm run bridge:health -- --timeout-ms 30000` 又恢复 ready,但直接 POST `agent_app_start_ui_runtime` 到 `/invoke` 仍 10s timeout。该 smoke 已终止,不计为通过证据。 +- 2026-05-16 22:40 前后,`agent_app_get_ui_runtime_status` 返回 `status=stopped / message=Agent App UI runtime 未启动`,但系统进程表仍有多条 cwd 为 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app` 的 `npm run dev`。这说明 Tauri runtime registry 与外部 App dev server 进程状态已经不一致;清理这些外部进程属于外部 App / Tauri runtime 写集,本进程不直接 kill。 +- 已补 `scripts/agent-apps-smoke.mjs` failure diagnostics:`try/catch` 会在 GUI smoke 失败时保存页面状态、DevBridge health、`agent_app_get_ui_runtime_status`、console errors、failed requests、failure screenshot 和外部 dev process snapshot;已用无效 app URL 自测确认 failure JSON / screenshot 会落盘,避免后续 Agent 再次人工复现才能定位 `launch-runtime-surface` 卡点。 +- 2026-05-16 22:58 复查 `npm run bridge:health -- --timeout-ms 30000` 仍超时,当前本机没有可用 `http://127.0.0.1:3030/health` DevBridge;为避免与隔壁 Tauri runtime 写集抢占,本进程未继续启动完整 `npm run verify:gui-smoke`。 +- 2026-05-16 23:00 后 DevBridge 恢复,focused Agent Apps smoke 与完整 `npm run verify:gui-smoke` 均已通过;此前 `agent_app_start_ui_runtime` timeout / registry stopped 的 GUI blocker 当前解除。 +- 2026-05-16 23:08 增强 `smoke:agent-apps`,新增 iframe 内内容工厂断言:Runtime frame 必须加载业务 App,并在“内容战役”页展示 Host capability profile / 模型、Token、费用和 Skills 统一回写提示。focused 复跑已通过。 +- 2026-05-16 23:20 前后继续补可选 `--include-content-factory-action-e2e` gate,用来验证真实业务按钮是否能进入 Host AgentRuntime。首轮失败后读取 failure JSON,确认 task id 实际已出现在 `lime.models/getRouting`、`lime.usage/*`、`lime.skills.list`、`lime.agent.streamTask/getTask` 等 SDK 后续调用中,只是 host task record 顶层未携带 task id。 +- 2026-05-16 23:35 修正 `scripts/agent-apps-smoke.mjs` 等待逻辑:task id 同时从 host task record 和 SDK call log 提取,并新增 runtime facts / stream-or-getTask 断言。 +- 2026-05-16 23:48 继续修正 smoke 诊断:读取 task-scoped host run record,而不只读 bridgeAction record;`agent-apps-smoke-p18-7-content-action-e2e-fixed5` 通过,默认 smoke `agent-apps-smoke-p18-7-default-after-action-gate-fixed4` 也通过。该证据把 P18.7-F 从“按钮只发出 startTask”推进到“最小真实按钮 E2E 已进入 Host AgentRuntime、可读取 runtimeFacts container,并能投影 required Skills”。 +- 2026-05-16 23:55 新增 `--include-content-factory-completion-e2e` 可选完成态 gate;当前运行 `agent-apps-smoke-p18-7-completion-gate-current` 预期失败,缺口明确为模型路由、Token、费用、Skill invocation、artifact、evidence 未完成,task record 仍显示 `active_turn_id=null / profile_status=idle`。 +- 2026-05-17 00:02 对同一 task 做只读根因探针:`agent_runtime_get_thread_read` 与 `agent_runtime_get_session` 均返回 `profile_status=idle / queued_turns_len=0 / turns_len=0 / messages_len=0`;SQLite `agent_sessions` 有 session 行,但 `agent_thread_turns`、`agent_messages`、`agent_runs` 均无对应记录;尝试 `agent_runtime_promote_queued_turn` 使用 `agent-app-queued-{taskId}` 返回 `false`。当前更像 accepted 后 runtime turn 未落库 / 未执行,而不是内容工厂 UI 未读取。 + +## Prompt-to-artifact 完成审计 + +| 显式要求 / gate | 对应 artifact / evidence | 覆盖结论 | +| --- | --- | --- | +| 多 Agent 并行时先切写集、避免夹写 | `AGENTS.md` 新增并行协作规则;`docs/aiprompts/parallel-agent-collaboration.md`;本文“并行写集”表 | 已覆盖;后续 Agent 应先读并声明写集 | +| 固定全量 `lime.*` capability surface 单一事实源 | `src/features/agent-app/sdk/capabilityCatalog.ts`;`capabilityContract.test.ts`;`capabilityAdapters.test.ts`;`publicSdkSurface.test.ts` | 已覆盖;catalog / adapter / profile 从同一事实源派生 | +| Agent App `manifestVersion: 0.6.0` 标准兼容 | `normalizeManifest.ts`、`checkReadiness.ts`、`parseManifest.test.ts`、`referenceCliCrossCheck.test.ts` | 已覆盖;v0.6 新字段和 runtime policy 深投影仍按 accepted divergence 退出条件跟踪 | +| `lime.capabilities` Host discovery | `capabilityDispatcher.test.ts`、`AgentAppRuntimePage.test.tsx` 覆盖 `list/get/getProfile` 与 iframe profile | Host 侧已覆盖;外部 App UI 消费未覆盖 | +| P18.7-D AgentRuntime resources | `capabilityDispatcher.test.ts` 覆盖 `models/usage/skills/memory/context` 只读投影与 mutation 拦截 | first-cut 已覆盖;真实预算、模型约束、memory query、context attach gate 未覆盖 | +| P18.7-E Tool / Integration | `capabilityDispatcher.test.ts` 覆盖 `search/browser/documents/media/mcp/terminal/connectors` 受控 intent / 只读投影 / not_available | Host first-cut 已覆盖;真实 ToolRuntime / Connector execution gate 未覆盖 | +| Agent Apps GUI 主路径可交付 | 2026-05-16 23:05 `npm run verify:gui-smoke` 通过;2026-05-16 23:08 focused `smoke:agent-apps` 新增 iframe 内 Host profile 断言后通过 | 已覆盖最新工作树;GUI 主路径当前复绿 | +| P18.7-F 内容工厂产品闭环 | 外部 `content-factory-app` tests / Host iframe profile smoke 已覆盖 typed Agent task、Host runtime facts、运行过程展示和写回路径;可选真实按钮 E2E 已证明 `lime.agent.startTask`、Host task accepted / runtimeFacts observed、models/usage/skills runtime facts 拉取和 stream 订阅启动 | first-cut 与最小真实按钮 E2E 已覆盖;真实业务 AI 动作完成态 / artifact / workspace patch 长链路未完成 | +| 验证失败可诊断 | `scripts/agent-apps-smoke.mjs` failure diagnostics;`agent-apps-smoke-diagnostics-selftest-failure.json/png` 与 `agent-apps-smoke-diagnostics-process-selftest-failure.json/png` 自测证据 | 已覆盖;后续失败应直接读取 failure JSON / screenshot,其中 `processSnapshot` 可直接确认是否存在 cwd 为 `content-factory-app` 的残留 dev server | + +## P18.7-F 完成度审计(2026-05-16 23:48) + +| 完成标准 | 当前证据 | 审计结论 | +| --- | --- | --- | +| 1. “整理知识库”真实通过 `lime.agent` 进入 AgentRuntime | `agent-apps-smoke-p18-7-content-action-e2e-fixed5-summary.json`:`contentFactoryActionStarted=true`、`contentFactoryActionTaskAccepted=true`、task id 来自 task-scoped host run record | 已覆盖最小按钮;其他页面按钮未覆盖 | +| 2. 运行过程展示来自 Host `runtimeProcess`,过程不消失 | 同一 summary:`contentFactoryActionProcessVisible=true`;host run record `runtimeProcess.timelineCount=1`,页面展示 `Lime AI 运行现场` | first-cut 覆盖;完成后折叠保持尚未覆盖 | +| 3. 模型、Token、费用、Skill、工具、引用、artifact、evidence 均可见 | `agent-apps-smoke-p18-7-completion-gate-current-failure.json`:`modelReady=false`、`usageReady=false`、`costReady=false`、`skillInvocationReady=false`、`artifactReady=false`、`evidenceReady=false`;task record 显示 `active_turn_id=null / profile_status=idle`;DevBridge/SQLite 探针显示该 session 无 turn/message/run | 未完成;已有可选 gate 固定该缺口,根因偏 runtime turn 未执行/未落库 | +| 4. 最终业务结果写回 App storage / artifacts / evidence | 完成态 gate 当前 `workspacePatchReady=true`,但 `artifactReady=false / evidenceReady=false`;外部 App 单测只能证明写回逻辑存在 | 未完成,不能用单测替代真实 Host iframe 完成态 | +| 5. 不跳回 Lime 通用 Chat,不直接调用模型 API | 当前 E2E 留在内容工厂 iframe,未出现 Host fallback;外部 App tests 覆盖 Host connected 时禁止本地生成 API | 当前按钮覆盖;全页面仍需逐个验证 | +| 6. standalone 页面不可替代 Host iframe 证据 | 现有 passing gate 均从 Lime Host iframe 触发;standalone 只作为外部 App 单测 / validate 辅证 | 已按口径执行 | + + +## 当前结论 + +- `current`:`lime.* capability catalog`、typed SDK、profile 派生、Agent App v0.6 reference cross-check、`lime.capabilities` Host discovery first-cut、`lime.models` / `lime.usage` / `lime.skills` / `lime.memory` / `lime.context` AgentRuntime 只读资源投影 first-cut、P18.7-E `lime.search` / `lime.browser` / `lime.documents` / `lime.media` / `lime.mcp` / `lime.terminal` / `lime.connectors` 受控工具 intent 与运行投影 first-cut,以及内容工厂 Host iframe profile / runtime facts 消费 first-cut。 +- `compat`:mock / adapter profile 和当前 Host Bridge preview 接线,仍服务测试与渐进接入。 +- `deprecated`:手写 capability 数组、App 自己解析 runtime 事件、复制底层工具执行逻辑。 +- `dead`:内容工厂专用后端命令、App 裸调模型 API、复制 Claw Skill launch。 + +## 完成度审计 + +| 目标 / 要求 | 当前证据 | 结论 | +| --- | --- | --- | +| P18.7-A:全量 `lime.*` catalog、typed SDK、profile 派生为单一事实源 | `capabilityCatalog.ts`、`capabilityContract.test.ts`、`capabilityAdapters.test.ts`、`publicSdkSurface.test.ts`;定向与 Agent App 全套通过 | 已完成 | +| P18.7-B:`manifestVersion: 0.6.0` 标准兼容、layered manifest 和 reference cross-check 复绿 | `normalizeManifest.ts`、`checkReadiness.ts`、`parseManifest.test.ts`、`referenceCliCrossCheck.test.ts`;v0.6 reference 相关定向通过 | 已完成 | +| P18.7-C:`lime.capabilities.list/get/getProfile` Host discovery,不泄露 internal path | `capabilityDispatcher.test.ts` 覆盖 list/get/getProfile、unknown capability、无 `path/sourceFile/internal`;Runtime Page test 覆盖 iframe 调用 `getProfile` | Host 侧 first-cut 已完成 | +| P18.7-C:业务 App UI 根据 profile 降级 | 只读审计外部 `content-factory-app`,当前未发现 `lime.capabilities` / `getProfile` 消费;外部仓库有大量并行脏写集 | 未完成,需外部 App 写集持有者接 | +| P18.7-D:`lime.usage` 投影 model、token、cost、budget | `capabilityDispatcher.test.ts` 覆盖 `getTokenUsage/getCostSummary` 从 `runtimeProcess` 投影 Token 与成本;`getBudget` 明确 `not_configured` | Token / cost first-cut 已完成;budget 未完成 | +| P18.7-D:`lime.models` 读取模型事实源、路由结果和约束 | `capabilityDispatcher.test.ts` 覆盖 `list/getRouting` 从 `runtimeProcess.model` 投影;`select/estimateCost` 为 runtime projection first-cut | first-cut 已完成,模型约束仍需接真实模型事实源 | +| P18.7-D:`lime.skills` | `capabilityDispatcher.test.ts` 覆盖 `list/resolve/getInvocation` 从 `runtimeProcess.skillNames/invokedSkillNames` 投影;`bind/invoke` 返回 `not_available`,不打开 mutation | first-cut 已完成,真实 workspace binding / Skill runtime gate 仍需后续主链接入 | +| P18.7-D:`lime.memory`、`lime.context` | `capabilityDispatcher.test.ts` 覆盖 `getStatus/query/getSnapshot` 从 App-scoped task、knowledge bindings、threadRead diagnostics、thread/turn ids 投影;`write/compact/attach/detach` 返回 `not_available`,不打开 mutation | first-cut 已完成,真实 memory runtime query / context attach gate 仍需后续主链接入 | +| P18.7-E:`lime.search/browser/documents/media/mcp/terminal/connectors` 工具集成 | `capabilityDispatcher.test.ts` 覆盖 `lime.search.query/getRun`、`lime.browser.open`、`lime.documents.parse`、`lime.media.generateImage`、`lime.mcp.listServers/invoke`、`lime.terminal.run/getRun/cancel`、`lime.connectors.list/getStatus/requestAuth/invoke`;Host Bridge 对工具执行只返回受控 `requires_agent_task` intent、只读投影、Host 授权需求或明确 `not_available`,不直接执行工具 / MCP / 终端 / 外部连接器 | Host 侧 first-cut 已完成;真实 ToolRuntime / Connector execution gate 仍未完成 | +| P18.7-F:内容工厂产品闭环复核 | Lime Host 侧已有 `lime.agent`、runtimeProcess、models/usage first-cut;外部 App `npm test` 覆盖 profile/usage/skills runtime facts、运行过程展示、workspace patch 写回、Host connected 时禁止本地生成 API;focused Agent Apps smoke 断言 iframe 内 Host profile 可见;可选真实按钮 E2E 已确认知识库整理按钮进入 Host AgentRuntime、拿到 task id、启动 runtime facts 拉取与 stream 订阅 | first-cut 与最小真实按钮 E2E 已完成;逐页面真实 AI 动作完成态 / artifact / workspace patch 长链路仍是后续深水位 | +| GUI 主路径可交付 | 2026-05-16 23:05 完整 `npm run verify:gui-smoke` 通过;2026-05-16 23:08 增强后 focused Agent Apps smoke 通过 `runtimeFrameContentFactoryLoaded/runtimeFrameHostProfileVisible` 断言 | 当前工作树已复绿 | + +## 剩余缺口 + +1. `docs/roadmap/agentapp/lime-capability-surface.md` 与 `p18-7-full-lime-capability-surface.md` 同时未跟踪,需由文档写集持有者决定是保留摘要还是合并入口。 +2. P18.7-B 的 v0.6 代码验证已通过当前工作树,但路线图文档还应补一条验证证据,避免后续 Agent 误判仍未复绿。 +3. P18.7-C first-cut 已可通过 Host Bridge 投影 catalog 摘要、stage、owner、enabled、implementation 和 unavailable reason;Agent App Runtime Page 已把 `lime.capabilities` 暴露给 iframe,并补了 `getProfile` 回归。只读审计外部 `content-factory-app` 后确认业务 App UI 尚未调用 `lime.capabilities` 做降级展示;该外部仓库当前有大量并行脏写集,本进程不接管。 +4. P18.7-D first-cut 已可通过 Host Bridge 为声明了 `lime.models` / `lime.usage` / `lime.skills` / `lime.memory` / `lime.context` 的 App 投影 `runtimeProcess`、App-scoped task、threadRead diagnostics、thread/turn ids 中的模型、Token、成本、Skill、记忆/上下文状态;预算尚无 AgentRuntime 事实源,当前明确返回 `status=not_configured / reason=no_agent_runtime_budget_facts`,`lime.skills.bind/invoke`、`lime.memory.write/compact`、`lime.context.attach/detach` 也明确返回 `not_available`,不伪造可变更或执行成功。 +5. P18.7-E 已完成 `search/browser/documents/media/mcp/terminal/connectors` Host first-cut,但真实 ToolRuntime / Connector execution gate 仍未完成;P18.7-D 深水位仍缺真实 memory runtime query、context attach gate、模型约束事实源、workspace skill binding ready 状态和预算事实。 +6. P18.7-F first-cut 已覆盖内容工厂 Host profile / runtime facts 消费和 GUI iframe 加载;当前可选深水位 smoke 已证明“整理知识库”按钮能发出 `lime.agent.startTask`、拿到 Host task id(taskIdSource=hostTaskRunRecord)、启动 runtime facts 拉取和 stream 订阅。但还缺 Host iframe 内逐个真实点击“生成场景 / 生成内容 / 只重写 / 生成脚本 / 交付 / 复盘”的长链路 E2E,以及完成态 artifact / evidence / workspace patch 回写证据。 + +## 下一刀归属 + +- 若隔壁仍持有外部 `content-factory-app`:业务 App UI 的 profile 降级展示应由隔壁接;本进程继续做 Lime Host 侧验证和小补丁。 +- 下一刀进入 P18.7-F 深水位:继续从已通过的 `--include-content-factory-action-e2e` 往后推进,补 runtimeProcess 中模型 / Token / 费用 / Skill invocation 的真实回写断言,再推进 artifact / evidence / workspace patch 完成态;若要覆盖更多页面,应按“生成场景 -> 生成内容 -> 交付 / 复盘”的顺序逐个加可选 gate。 diff --git a/docs/roadmap/agentapp/p18-completion-audit.md b/docs/roadmap/agentapp/p18-completion-audit.md index f8dea616f..48fda0134 100644 --- a/docs/roadmap/agentapp/p18-completion-audit.md +++ b/docs/roadmap/agentapp/p18-completion-audit.md @@ -2,7 +2,11 @@ 更新时间:2026-05-16 -状态:P18 当前交付候选已具备证据;P18.1、P18.2、P18.3、P18.4、P18.4-H、P18.5.1、P18.5.2、P18.5-S 与 P18.6 均有当前证据,完整 `verify:local` 已端到端通过;P18.5.3 package-side SDK facade 已从 `blocked` 推进到真实外部 package `npm run verify` 通过,且 handoff gate 显示 `distArtifacts=0`。当前剩余不是功能实现缺口,而是外部 package / Lime 工作区仍 dirty,需要 owner handoff / commit 边界收口。 +状态:P18.1-P18.6 当前交付候选已具备证据;P18.7-A 全量 capability catalog / SDK / profile 派生已落地,P18.7-B Agent App v0.6 reference compatibility 已复绿。当前剩余主线不是 P18.5 owner handoff,而是 P18.7-C Host capability discovery、P18.7-D/E 后端能力接线和 P18.7-F 内容工厂真实闭环复核;外部 package / Lime 工作区 dirty 仍需要 owner 提交边界收口。 + +## P18.7 追加审计口径 + +2026-05-16 追加:本审计原本判定的是 P18 typed SDK gate,而不是“Lime 所有能力已完成抽象和后端接线”。用户明确要求继续完善到整体目标后,完成口径改为 [P18.7 Full Lime Capability Surface](./p18-7-full-lime-capability-surface.md):全量 capability catalog、v0.6 reference 兼容、Host discovery、AgentRuntime resource 能力、Tool/Integration 能力和内容工厂真实闭环都要逐项有证据。当前 P18.7-A / P18.7-B 已完成;P18.7-C / D / E / F 仍是未完成主线。 ## 一句话结论 @@ -10,14 +14,15 @@ P18 的 Lime-side typed Capability SDK gate 已具备当前可验证证据,并 ## 当前绿灯 / 阻塞摘要 -| 分类 | 最新证据 | 判定 | -|---|---|---| -| Lime-side Agent App feature island | 2026-05-16 10:17 `npm test -- src/features/agent-app`:36 files / 178 tests passed;2026-05-16 10:41 复跑 SDK seam 最小集 4 files / 11 tests passed。 | 通过当前定向测试;包含 SDK-only public surface、public SDK export regression 与内容工厂 SDK regression。 | -| P18 SDK / Host Bridge | 2026-05-16 07:14 P18 SDK / Host Bridge 定向测试:9 files / 43 tests passed。 | 通过当前定向测试。 | -| 静态与契约 | 2026-05-16 09:07 `verify:app-version` 已重跑通过;10:20 `lint` 通过;10:15 `typecheck` 通过;10:21 `test:contracts` 通过。 | 通过当前本地非 GUI 验证。 | -| 外部 `content-factory-app` 只读验证 | 2026-05-16 10:41 低优先级复跑 `npm test`:46 tests passed;`validate:app` 为 `passed`,manifest hash `sha256:6ec3fed5f163739bcf0fd2b845c51a8e10d28aa856e8c6f90259fdab9edd1e48`;`readiness:app` 为 `needs-setup`。 | 只读验证通过;不能替代会重建 `dist/*` 的 package verify。 | -| P18.5.3 package-side SDK facade / verify | 2026-05-16 10:55 真实外部 package `npm run verify` 通过:build、46 tests、validate、readiness 均完成;随后 handoff gate 输出 `status=needs_handoff`、dirty `tracked=36 / untracked=5`、`hostBridgePrivate=none`、`uiTestPrivate=none`、SDK marker 命中 `2+2`、`distArtifacts=diff:0,missing:0,extra:0,total:0`。 | 功能和产物验收通过;仍需 owner handoff,因为 package worktree 仍 dirty 且 build/verify/e2e 脚本天然会重写 dist。 | -| 完整 `verify:local` | 2026-05-16 07:33 低优先级运行 `nice -n 10 npm run verify:local` 端到端通过,覆盖 app version、lint、typecheck、Vitest smart 58 batches、`test:contracts`、`cargo test --manifest-path src-tauri/Cargo.toml` 与全套 GUI smoke;`smoke:claw-chat-ready-streaming` 同次重跑 summary 为 `verdict=pass / error=null / consoleErrorCount=0 / failureRuntime=null`。 | 全量本地验证通过;不再是 P18 当前阻塞项。 | +| 分类 | 最新证据 | 判定 | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Lime-side Agent App feature island | 2026-05-16 10:17 `npm test -- src/features/agent-app`:36 files / 178 tests passed;2026-05-16 10:41 复跑 SDK seam 最小集 4 files / 11 tests passed。 | 通过当前定向测试;包含 SDK-only public surface、public SDK export regression 与内容工厂 SDK regression。 | +| P18 SDK / Host Bridge | 2026-05-16 07:14 P18 SDK / Host Bridge 定向测试:9 files / 43 tests passed。 | 通过当前定向测试。 | +| 静态与契约 | 2026-05-16 09:07 `verify:app-version` 已重跑通过;10:20 `lint` 通过;10:15 `typecheck` 通过;10:21 `test:contracts` 通过。 | 通过当前本地非 GUI 验证。 | +| 外部 `content-factory-app` 只读验证 | 2026-05-16 10:41 低优先级复跑 `npm test`:46 tests passed;`validate:app` 为 `passed`,manifest hash `sha256:6ec3fed5f163739bcf0fd2b845c51a8e10d28aa856e8c6f90259fdab9edd1e48`;`readiness:app` 为 `needs-setup`。 | 只读验证通过;不能替代会重建 `dist/*` 的 package verify。 | +| P18.5.3 package-side SDK facade / verify | 2026-05-16 10:55 真实外部 package `npm run verify` 通过:build、46 tests、validate、readiness 均完成;随后 handoff gate 输出 `status=needs_handoff`、dirty `tracked=36 / untracked=5`、`hostBridgePrivate=none`、`uiTestPrivate=none`、SDK marker 命中 `2+2`、`distArtifacts=diff:0,missing:0,extra:0,total:0`。 | 功能和产物验收通过;仍需 owner handoff,因为 package worktree 仍 dirty 且 build/verify/e2e 脚本天然会重写 dist。 | +| P18.7-A / P18.7-B capability surface / v0.6 reference | 2026-05-16 当前会话新增 `capabilityCatalog` 事实源、catalog/profile/adapter 派生测试;`normalizeManifest` 支持 `0.6.x`,`checkReadiness` 支持 `"0.6"`,`referenceCliCrossCheck.test.ts` 兼容 capability list、layered manifest、`agentRuntime` 和 v0.6 schema divergences;定向测试、typecheck、contracts、legacy report 均通过。 | P18.7-A/B 通过;不代表 Host discovery、后端 resource 接线或内容工厂真实闭环完成。 | +| 完整 `verify:local` | 2026-05-16 07:33 低优先级运行 `nice -n 10 npm run verify:local` 端到端通过,覆盖 app version、lint、typecheck、Vitest smart 58 batches、`test:contracts`、`cargo test --manifest-path src-tauri/Cargo.toml` 与全套 GUI smoke;`smoke:claw-chat-ready-streaming` 同次重跑 summary 为 `verdict=pass / error=null / consoleErrorCount=0 / failureRuntime=null`。 | 全量本地验证通过;不再是 P18 当前阻塞项。 | ## 目标重述 @@ -32,33 +37,33 @@ P18 要证明 Agent App 可以只依赖 Lime 公开 SDK facade 和 Host Bridge v ## Prompt-to-artifact checklist -| 要求 | 当前证据 | 审计判定 | -|---|---|---| -| P18.1 Typed SDK facade、stable error、mock host。 | `src/features/agent-app/sdk/capabilityContract.ts`、`src/features/agent-app/sdk/capabilityErrors.ts`、`src/features/agent-app/sdk/MockCapabilityHost.ts`、`src/features/agent-app/sdk/capabilityContract.test.ts`、`src/features/agent-app/sdk/MockCapabilityHost.test.ts`。 | 通过当前 Lime-side 契约验证。 | -| P18.2 Host Bridge typed router / stable error response。 | `src/features/agent-app/runtime/hostBridge.ts`、`src/features/agent-app/runtime/capabilityDispatcher.ts`、`hostBridge.test.ts`、`capabilityDispatcher.test.ts`。 | 通过当前定向测试;未进入 GUI smoke。 | -| P18.3 Core capability adapters。 | `src/features/agent-app/sdk/capabilityAdapters.ts` 覆盖 `lime.ui / storage / artifacts / evidence / knowledge / tools / agent` facade;`capabilityAdapters.test.ts` 覆盖 adapter call envelope。 | 通过当前定向测试。 | -| P18.4 App-scoped Agent task SDK facade。 | `LimeAgentCapabilityAdapter` 暴露 `startTask / streamTask / getTask / cancelTask / retryTask / submitHostResponse / listTasks`;`agentRuntimeCapabilityHost.test.ts` 覆盖 runtime host 接线。 | 通过当前定向测试;后端 push subscribe 仍归 AgentRuntime owner。 | -| P18.4-H AgentRuntime handoff gate。 | `docs/roadmap/agentapp/p18-4-h-agentruntime-handoff-gate.md` 记录 handoff 判定与 owner 缺口。 | 文档 gate 已有;不由 P18 复制 runtime read model。 | -| P18.5.1 Lime-side 内容工厂 SDK regression。 | `src/features/agent-app/sdk/contentFactorySdkRegression.test.ts` 断言内容工厂主链调用 `lime.agent.startTask / streamTask / submitHostResponse / storage.set / artifacts.create / evidence.record`,且 2026-05-16 10:08 已改为只从 `src/features/agent-app/sdk/index.ts` 的 SDK-only public surface 导入类型与 helper。 | 通过当前定向测试;证明 Lime-side 内容工厂回归不依赖 SDK 内部深文件。 | -| P18.5.2 外部 package read-only tests。 | `/Users/coso/Documents/dev/ai/limecloud/content-factory-app: npm test` 当前通过;2026-05-16 08:19 owner handoff gate 复核为 46 tests passed。 | 通过只读测试;不能替代 package-side SDK facade 或 verify。 | -| P18.5.2 外部 package 标准只读校验。 | 2026-05-16 07:11 与 08:42 低优先级运行 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app: npm run validate:app && npm run readiness:app`;`validate:app` 返回 `ok=true / status=passed`,manifest hash 为 `sha256:6ec3fed5f163739bcf0fd2b845c51a8e10d28aa856e8c6f90259fdab9edd1e48`;`readiness:app` 返回 `ok=true / status=needs-setup`。 | 通过只读标准校验;`needs-setup` 符合宿主需补齐 required 依赖的状态,仍不能替代会重写 `dist/*` 的 package verify。 | -| P18.5-S Lime-side Host Bridge SDK client。 | `src/features/agent-app/sdk/hostBridgeClient.ts` 提供 `createLimeHostBridgeCapabilityInvoker`,可把标准 `createLimeCoreCapabilityAdapters` 调用转成 Host Bridge v1 `capability:invoke` envelope,并承接 ready / snapshot / theme / visibility / toast / navigate / openExternal / download / `capability:subscribe / unsubscribe / event`;`hostBridgeClient.test.ts` 覆盖 Host action、trusted origin、stable error、timeout cleanup、subscription 分发和内容工厂 task / write-back / artifact / evidence 主链。 | 通过当前定向测试;为外部 package 收敛提供目标,但不等于已改外部 package。 | -| P18.5-S SDK-only public surface。 | `src/features/agent-app/sdk/index.ts` 只导出 capability facade、stable error、Host Bridge client、mock host 和 App package 需要的 task / storage / artifact / evidence 类型;`src/features/agent-app/sdk/publicSdkSurface.test.ts` 固定该窄导出清单,并从运行时 namespace 与源码 export 来源两侧断言不导出 UI、installer、repository、runtime host、adapter、schema 或 dispatcher。 | 2026-05-16 10:08 定向测试通过,10:15 typecheck 通过;降低后续 `@lime/app-sdk` / package-local shim 误导出内部实现的风险。 | -| P18.5-S public SDK export seam。 | `src/features/agent-app/index.ts` 已从 feature public entry 导出 `createLimeCoreCapabilityAdapters`、`createLimeHostBridgeCapabilityInvoker`、`LIME_AGENT_APP_BRIDGE_PROTOCOL` 与 `LIME_AGENT_APP_BRIDGE_VERSION`;`src/features/agent-app/index.test.ts` 固定该 public export contract。 | 通过 2026-05-16 07:54 定向测试;防止外部 package 后续只能依赖 Lime 内部深路径。 | -| Host Bridge SDK client 文档化。 | `docs/roadmap/agentapp/capability-sdk.md` 增加 Host Bridge SDK Client 章节,记录 package-side App 如何用 `createLimeHostBridgeCapabilityInvoker + createLimeCoreCapabilityAdapters`,以及 P18.5.3 迁移规则。 | 通过文档 diff check;仍不替代外部 package 改造。 | -| P18.5.3 外部 package 迁移计划。 | `docs/roadmap/agentapp/p18-5-3-package-sdk-migration-plan.md` 固定外部 package 当前只读事实、推荐写集、迁移步骤、验收清单和回滚点。 | 通过文档 diff check;等待外部 package owner 稳定后执行。 | -| P18.5.3 owner handoff 单页。 | `docs/roadmap/agentapp/p18-5-3-owner-handoff.md` 浓缩接管前命令、最小写集、禁止项、迁移目标、验证顺序和完成条件。 | 给外部 package owner / 后续执行者使用;不替代 handoff 确认,也不代表外部 package 已迁移。 | -| P18.5.3 最新复核。 | 2026-05-16 10:55 真实外部 package `npm run verify` 通过;handoff gate 显示 `hostBridgePrivate=none`、`uiTestPrivate=none`、SDK marker 命中 2+2、`distArtifacts=0`、blockers 为 `none`。 | P18.5.3 SDK facade / package verify / dist 同步已完成;当前只剩 owner handoff。 | -| P18.5.3 package verify 风险。 | `docs/roadmap/agentapp/p18-5-3-package-sdk-migration-plan.md` 已记录 `npm run verify` 会先执行 `npm run build` 并重建 `dist/*`;2026-05-16 10:55 用户确认后已在真实 package 执行一次 verify。 | 当前 verify 已完成;后续若再次运行 build / verify / e2e,仍需 owner 接受 dist 重建。 | -| P18.5.3 外部 UI tests 迁移靶点。 | `docs/roadmap/agentapp/p18-5-3-package-sdk-migration-plan.md` 已记录 `tests/ui.test.mjs` 中需要调整的 Host Bridge 断言:保留业务闭环、manifest kind 和 capability call sequence,删除鼓励手写 `postMessage` / message type 的结构性断言。 | 后续 package-side 迁移时可只改测试 mock / facade 断言,不删除内容工厂业务行为断言。 | -| P18.5.3 package-side facade 兼容导出。 | `docs/roadmap/agentapp/p18-5-3-package-sdk-migration-plan.md` 已记录外部 `src/ui/app.js` 只直接 import `initHostBridge / notifyHost / runHostAgentTask / syncHostConfirmation / writeHostTaskResult`。 | 后续迁移优先保持这 5 个业务侧导出稳定,避免同时改 UI 主流程。 | -| P18.5.3 package-side 迁移 dry-run 设计。 | `docs/roadmap/agentapp/p18-5-3-package-sdk-migration-plan.md` 已基于当前外部 `src/ui/host-bridge.js` 只读结构,记录最小 diff 顺序:SDK runtime factory、pure helper 保留、私有 transport owner 删除 / 下沉、业务 helper 只调用 `lime.agent / storage / artifacts / evidence` facade。 | 等 handoff 后可按设计实施;当前仍未应用外部 package。 | -| P18.5.3 package-local SDK shim 退出条件。 | 外部 `content-factory-app` 当前 `dependencies / devDependencies` 为空;`p18-5-3-package-sdk-migration-plan.md` 已把 `src/ui/lime-app-sdk.js` 或等价 shim 列为仅在正式 `@lime/app-sdk` 不可安装时的临时写集。 | 禁止 import Lime repo 内部路径;shim 只能暴露 SDK 等价最小接口,后续发布正式 SDK 后退出。 | -| 正式 `@lime/app-sdk` package 发布状态。 | 2026-05-16 08:37 只读复核:Lime repo 根 `package.json` 仍为 `"private": true`;`packages/` 下只有 `packages/lime-cli-npm`;未发现独立 `@lime/app-sdk` / `lime-app-sdk` package。 | 不把正式 SDK package 发布当作 P18.5.3 已完成证据;外部 App 仍不得 import Lime internal path,必要时只能使用带退出条件的 package-local shim。 | -| P18.5.3 package-side SDK facade / verify。 | 2026-05-16 10:55 真实外部 package `npm run verify` 已通过,且 handoff gate 确认 `distArtifacts=0`。 | package-side SDK facade / verify 已完成;剩余为 owner handoff / git 写集收口,不是功能 blocker。 | -| P18.6 Raw Worker 前 Gate。 | `workflowRuntimeHost.test.ts`、`featureFlag.test.ts`、`runtimePackageLoader.test.ts`、`entryRuntimeGuard.test.ts` 当前通过 25 tests;2026-05-16 08:33 feature island 越界扫描无输出。 | 通过当前 gate;raw worker 后移 P19。 | -| 不绕过 SDK / 不复活旧 SceneApp。 | `rg -n "SceneApp|contentEngineering|sceneapp_|safeInvoke|invoke\\(|new Worker|Worker\\(" src/features/agent-app || true` 无输出。 | 通过当前扫描。 | -| 不抢隔壁运行任务。 | 本审计只读外部 package;不改 `src-tauri/*`、`src/lib/api/agentAppRuntime.ts`、`agentRuntimeCapabilityHost*` 实现逻辑、GUI smoke 脚本或外部 package。 | 符合当前协作边界。 | +| 要求 | 当前证据 | 审计判定 | +| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ---------- | --------- | ---------- | --------------------------------- | --- | -------------- | -------------- | +| P18.1 Typed SDK facade、stable error、mock host。 | `src/features/agent-app/sdk/capabilityContract.ts`、`src/features/agent-app/sdk/capabilityErrors.ts`、`src/features/agent-app/sdk/MockCapabilityHost.ts`、`src/features/agent-app/sdk/capabilityContract.test.ts`、`src/features/agent-app/sdk/MockCapabilityHost.test.ts`。 | 通过当前 Lime-side 契约验证。 | +| P18.2 Host Bridge typed router / stable error response。 | `src/features/agent-app/runtime/hostBridge.ts`、`src/features/agent-app/runtime/capabilityDispatcher.ts`、`hostBridge.test.ts`、`capabilityDispatcher.test.ts`。 | 通过当前定向测试;未进入 GUI smoke。 | +| P18.3 Core capability adapters。 | `src/features/agent-app/sdk/capabilityAdapters.ts` 覆盖 `lime.ui / storage / artifacts / evidence / knowledge / tools / agent` facade;`capabilityAdapters.test.ts` 覆盖 adapter call envelope。 | 通过当前定向测试。 | +| P18.4 App-scoped Agent task SDK facade。 | `LimeAgentCapabilityAdapter` 暴露 `startTask / streamTask / getTask / cancelTask / retryTask / submitHostResponse / listTasks`;`agentRuntimeCapabilityHost.test.ts` 覆盖 runtime host 接线。 | 通过当前定向测试;后端 push subscribe 仍归 AgentRuntime owner。 | +| P18.4-H AgentRuntime handoff gate。 | `docs/roadmap/agentapp/p18-4-h-agentruntime-handoff-gate.md` 记录 handoff 判定与 owner 缺口。 | 文档 gate 已有;不由 P18 复制 runtime read model。 | +| P18.5.1 Lime-side 内容工厂 SDK regression。 | `src/features/agent-app/sdk/contentFactorySdkRegression.test.ts` 断言内容工厂主链调用 `lime.agent.startTask / streamTask / submitHostResponse / storage.set / artifacts.create / evidence.record`,且 2026-05-16 10:08 已改为只从 `src/features/agent-app/sdk/index.ts` 的 SDK-only public surface 导入类型与 helper。 | 通过当前定向测试;证明 Lime-side 内容工厂回归不依赖 SDK 内部深文件。 | +| P18.5.2 外部 package read-only tests。 | `/Users/coso/Documents/dev/ai/limecloud/content-factory-app: npm test` 当前通过;2026-05-16 08:19 owner handoff gate 复核为 46 tests passed。 | 通过只读测试;不能替代 package-side SDK facade 或 verify。 | +| P18.5.2 外部 package 标准只读校验。 | 2026-05-16 07:11 与 08:42 低优先级运行 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app: npm run validate:app && npm run readiness:app`;`validate:app` 返回 `ok=true / status=passed`,manifest hash 为 `sha256:6ec3fed5f163739bcf0fd2b845c51a8e10d28aa856e8c6f90259fdab9edd1e48`;`readiness:app` 返回 `ok=true / status=needs-setup`。 | 通过只读标准校验;`needs-setup` 符合宿主需补齐 required 依赖的状态,仍不能替代会重写 `dist/*` 的 package verify。 | +| P18.5-S Lime-side Host Bridge SDK client。 | `src/features/agent-app/sdk/hostBridgeClient.ts` 提供 `createLimeHostBridgeCapabilityInvoker`,可把标准 `createLimeCoreCapabilityAdapters` 调用转成 Host Bridge v1 `capability:invoke` envelope,并承接 ready / snapshot / theme / visibility / toast / navigate / openExternal / download / `capability:subscribe / unsubscribe / event`;`hostBridgeClient.test.ts` 覆盖 Host action、trusted origin、stable error、timeout cleanup、subscription 分发和内容工厂 task / write-back / artifact / evidence 主链。 | 通过当前定向测试;为外部 package 收敛提供目标,但不等于已改外部 package。 | +| P18.5-S SDK-only public surface。 | `src/features/agent-app/sdk/index.ts` 只导出 capability facade、stable error、Host Bridge client、mock host 和 App package 需要的 task / storage / artifact / evidence 类型;`src/features/agent-app/sdk/publicSdkSurface.test.ts` 固定该窄导出清单,并从运行时 namespace 与源码 export 来源两侧断言不导出 UI、installer、repository、runtime host、adapter、schema 或 dispatcher。 | 2026-05-16 10:08 定向测试通过,10:15 typecheck 通过;降低后续 `@lime/app-sdk` / package-local shim 误导出内部实现的风险。 | +| P18.5-S public SDK export seam。 | `src/features/agent-app/index.ts` 已从 feature public entry 导出 `createLimeCoreCapabilityAdapters`、`createLimeHostBridgeCapabilityInvoker`、`LIME_AGENT_APP_BRIDGE_PROTOCOL` 与 `LIME_AGENT_APP_BRIDGE_VERSION`;`src/features/agent-app/index.test.ts` 固定该 public export contract。 | 通过 2026-05-16 07:54 定向测试;防止外部 package 后续只能依赖 Lime 内部深路径。 | +| Host Bridge SDK client 文档化。 | `docs/roadmap/agentapp/capability-sdk.md` 增加 Host Bridge SDK Client 章节,记录 package-side App 如何用 `createLimeHostBridgeCapabilityInvoker + createLimeCoreCapabilityAdapters`,以及 P18.5.3 迁移规则。 | 通过文档 diff check;仍不替代外部 package 改造。 | +| P18.5.3 外部 package 迁移计划。 | `docs/roadmap/agentapp/p18-5-3-package-sdk-migration-plan.md` 固定外部 package 当前只读事实、推荐写集、迁移步骤、验收清单和回滚点。 | 通过文档 diff check;等待外部 package owner 稳定后执行。 | +| P18.5.3 owner handoff 单页。 | `docs/roadmap/agentapp/p18-5-3-owner-handoff.md` 浓缩接管前命令、最小写集、禁止项、迁移目标、验证顺序和完成条件。 | 给外部 package owner / 后续执行者使用;不替代 handoff 确认,也不代表外部 package 已迁移。 | +| P18.5.3 最新复核。 | 2026-05-16 10:55 真实外部 package `npm run verify` 通过;handoff gate 显示 `hostBridgePrivate=none`、`uiTestPrivate=none`、SDK marker 命中 2+2、`distArtifacts=0`、blockers 为 `none`。 | P18.5.3 SDK facade / package verify / dist 同步已完成;当前只剩 owner handoff。 | +| P18.5.3 package verify 风险。 | `docs/roadmap/agentapp/p18-5-3-package-sdk-migration-plan.md` 已记录 `npm run verify` 会先执行 `npm run build` 并重建 `dist/*`;2026-05-16 10:55 用户确认后已在真实 package 执行一次 verify。 | 当前 verify 已完成;后续若再次运行 build / verify / e2e,仍需 owner 接受 dist 重建。 | +| P18.5.3 外部 UI tests 迁移靶点。 | `docs/roadmap/agentapp/p18-5-3-package-sdk-migration-plan.md` 已记录 `tests/ui.test.mjs` 中需要调整的 Host Bridge 断言:保留业务闭环、manifest kind 和 capability call sequence,删除鼓励手写 `postMessage` / message type 的结构性断言。 | 后续 package-side 迁移时可只改测试 mock / facade 断言,不删除内容工厂业务行为断言。 | +| P18.5.3 package-side facade 兼容导出。 | `docs/roadmap/agentapp/p18-5-3-package-sdk-migration-plan.md` 已记录外部 `src/ui/app.js` 只直接 import `initHostBridge / notifyHost / runHostAgentTask / syncHostConfirmation / writeHostTaskResult`。 | 后续迁移优先保持这 5 个业务侧导出稳定,避免同时改 UI 主流程。 | +| P18.5.3 package-side 迁移 dry-run 设计。 | `docs/roadmap/agentapp/p18-5-3-package-sdk-migration-plan.md` 已基于当前外部 `src/ui/host-bridge.js` 只读结构,记录最小 diff 顺序:SDK runtime factory、pure helper 保留、私有 transport owner 删除 / 下沉、业务 helper 只调用 `lime.agent / storage / artifacts / evidence` facade。 | 等 handoff 后可按设计实施;当前仍未应用外部 package。 | +| P18.5.3 package-local SDK shim 退出条件。 | 外部 `content-factory-app` 当前 `dependencies / devDependencies` 为空;`p18-5-3-package-sdk-migration-plan.md` 已把 `src/ui/lime-app-sdk.js` 或等价 shim 列为仅在正式 `@lime/app-sdk` 不可安装时的临时写集。 | 禁止 import Lime repo 内部路径;shim 只能暴露 SDK 等价最小接口,后续发布正式 SDK 后退出。 | +| 正式 `@lime/app-sdk` package 发布状态。 | 2026-05-16 08:37 只读复核:Lime repo 根 `package.json` 仍为 `"private": true`;`packages/` 下只有 `packages/lime-cli-npm`;未发现独立 `@lime/app-sdk` / `lime-app-sdk` package。 | 不把正式 SDK package 发布当作 P18.5.3 已完成证据;外部 App 仍不得 import Lime internal path,必要时只能使用带退出条件的 package-local shim。 | +| P18.5.3 package-side SDK facade / verify。 | 2026-05-16 10:55 真实外部 package `npm run verify` 已通过,且 handoff gate 确认 `distArtifacts=0`。 | package-side SDK facade / verify 已完成;剩余为 owner handoff / git 写集收口,不是功能 blocker。 | +| P18.6 Raw Worker 前 Gate。 | `workflowRuntimeHost.test.ts`、`featureFlag.test.ts`、`runtimePackageLoader.test.ts`、`entryRuntimeGuard.test.ts` 当前通过 25 tests;2026-05-16 08:33 feature island 越界扫描无输出。 | 通过当前 gate;raw worker 后移 P19。 | +| 不绕过 SDK / 不复活旧 SceneApp。 | `rg -n "SceneApp | contentEngineering | sceneapp\_ | safeInvoke | invoke\\( | new Worker | Worker\\(" src/features/agent-app | | true` 无输出。 | 通过当前扫描。 | +| 不抢隔壁运行任务。 | 本审计只读外部 package;不改 `src-tauri/*`、`src/lib/api/agentAppRuntime.ts`、`agentRuntimeCapabilityHost*` 实现逻辑、GUI smoke 脚本或外部 package。 | 符合当前协作边界。 | ## 本轮复核命令 @@ -109,68 +114,68 @@ nice -n 10 npm run harness:doc-freshness 本段只记录证据,不接管 AgentRuntime / GUI smoke owner 写集;最新完整 `verify:local` 已消费该 pass 结果。 -| 证据 | 观察 | P18 判定 | -|---|---|---| -| Summary | `.lime/qc/gui-evidence/claw-chat-ready-streaming/claw-chat-ready-streaming-summary.json` 在 2026-05-16 07:33 更新为 `verdict=pass / error=null / consoleErrorCount=0 / failureRuntime=null`;只读复核显示 `providerPreference=deepseek`、`modelPreference=deepseek-v4-flash`、`interruptedTurnStatus=aborted`、`followSessionId=c2742b86-f2cc-4a0b-91f2-b1baf3c6005e`、`followTurnId=091b4738-a537-43f2-a4b9-daf0ed1b3c3d`。 | 历史 blocker 已解除,并已进入完整 `verify:local` 通过证据;P18 不需要抢修 runtime / smoke。 | -| GUI smoke 链路 | 同次完整 `verify:local` 已跑过 workspace-ready、browser-runtime、site-adapters、agent-service-skill-entry、agent-runtime-tool-surface、agent-runtime-tool-surface-page、at-command-registry、agent-apps、claw-chat-ready-streaming、knowledge-gui 与 design-canvas。 | 全局 GUI smoke 不再阻塞 P18 当前 SDK gate;后续只在外部 package 迁移后按改动范围补验证。 | -| 协作边界 | 本轮不改 `src-tauri/*`、runtime facade 或 smoke 脚本;只把已通过证据写入客户端 P18 审计。 | 避免和隔壁 AgentRuntime / GUI smoke 任务打架。 | +| 证据 | 观察 | P18 判定 | +| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| Summary | `.lime/qc/gui-evidence/claw-chat-ready-streaming/claw-chat-ready-streaming-summary.json` 在 2026-05-16 07:33 更新为 `verdict=pass / error=null / consoleErrorCount=0 / failureRuntime=null`;只读复核显示 `providerPreference=deepseek`、`modelPreference=deepseek-v4-flash`、`interruptedTurnStatus=aborted`、`followSessionId=c2742b86-f2cc-4a0b-91f2-b1baf3c6005e`、`followTurnId=091b4738-a537-43f2-a4b9-daf0ed1b3c3d`。 | 历史 blocker 已解除,并已进入完整 `verify:local` 通过证据;P18 不需要抢修 runtime / smoke。 | +| GUI smoke 链路 | 同次完整 `verify:local` 已跑过 workspace-ready、browser-runtime、site-adapters、agent-service-skill-entry、agent-runtime-tool-surface、agent-runtime-tool-surface-page、at-command-registry、agent-apps、claw-chat-ready-streaming、knowledge-gui 与 design-canvas。 | 全局 GUI smoke 不再阻塞 P18 当前 SDK gate;后续只在外部 package 迁移后按改动范围补验证。 | +| 协作边界 | 本轮不改 `src-tauri/*`、runtime facade 或 smoke 脚本;只把已通过证据写入客户端 P18 审计。 | 避免和隔壁 AgentRuntime / GUI smoke 任务打架。 | ## 当前协作分工 -| 工作面 | Owner | P18 处理方式 | -|---|---|---| -| 外部 `content-factory-app` 源码、dist、UI、模型生成与 docs。 | 隔壁 package 任务。 | 只读观察;不覆盖 dirty / untracked 文件。 | -| AgentRuntime Rust / TS facade、push subscribe、workspace patch producer、capability catalog、真实桌面 GUI smoke。 | 隔壁 AgentRuntime 任务。 | 只消费已验证 host surface,不复制 read model 或 runtime command。 | -| Agent App P18 SDK contract、stable error、mock host、typed adapters、Lime-side regression、Host Bridge SDK client。 | Agent App P18。 | 当前通过定向测试与 contracts。 | -| P18.5.3 package-side SDK facade / verify。 | 等外部 package owner 稳定后认领。 | 当前 blocked;不能用 read-only tests 伪装完成。 | +| 工作面 | Owner | P18 处理方式 | +| ------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ----------------------------------------------------------------- | +| 外部 `content-factory-app` 源码、dist、UI、模型生成与 docs。 | 隔壁 package 任务。 | 只读观察;不覆盖 dirty / untracked 文件。 | +| AgentRuntime Rust / TS facade、push subscribe、workspace patch producer、capability catalog、真实桌面 GUI smoke。 | 隔壁 AgentRuntime 任务。 | 只消费已验证 host surface,不复制 read model 或 runtime command。 | +| Agent App P18 SDK contract、stable error、mock host、typed adapters、Lime-side regression、Host Bridge SDK client。 | Agent App P18。 | 当前通过定向测试与 contracts。 | +| P18.5.3 package-side SDK facade / verify。 | 等外部 package owner 稳定后认领。 | 当前 blocked;不能用 read-only tests 伪装完成。 | ## 2026-05-16 协作锁定 隔壁标准资料已有新增,且外部 package / AgentRuntime 仍在并行推进。P18 当前采取“只读同步标准、只写客户端计划、不抢运行写集”的分工: -| 输入 / 写集 | 当前观察 | 本轮处理方式 | -|---|---|---| -| `/Users/coso/Documents/dev/ai/limecloud/agentapp` | 工作区干净;当前 `HEAD=4bef605 fix: quote mermaid sdk labels`,zh / en 标准资料已覆盖 authoring、client implementation、reference、examples、what-is-agent-app、agent-app-vs-skills-knowledge 与 mini-program analogy,进一步固定 Agent App 是完整应用包、Expert Chat 只是 entry、能力调用必须通过 Capability SDK。 | 作为标准资料只读输入;同步到本审计和后续计划口径,不改标准仓库、不复制标准文档。 | -| `/Users/coso/Documents/dev/ai/limecloud/content-factory-app` | 2026-05-16 07:39 只读复核有 39 个 dirty / untracked 条目,其中 36 个 tracked modified、3 个 untracked;当时 `src/ui/host-bridge.js`、`dist/ui/host-bridge.js`、`src/ui/app.js`、`tests/ui.test.mjs`、`dist/*`、docs 和 model-generation 相关文件仍在并行改动,且私有 bridge marker 仍命中。 | 历史 blocker;已被 10:55 真实 package verify 和 handoff gate `blockers=none / distArtifacts=0` 覆盖。 | -| `src-tauri/*`、`src/lib/api/agentAppRuntime.ts`、`src/features/agent-app/runtime/agentRuntimeCapabilityHost*`、GUI smoke 脚本 | 2026-05-16 07:05 只读复核 owner-sensitive 写集仍有 25 个 dirty / untracked 条目,属于 AgentRuntime / GUI owner 面。 | 不改;P18 只消费已验证 host surface,不抢 Rust / TS facade / GUI smoke。 | -| `smoke:claw-chat-ready-streaming` | 2026-05-16 07:33 只读复核最新 summary 为 `verdict=pass / error=null`;`providerPreference=deepseek`、`modelPreference=deepseek-v4-flash`、`interruptedTurnStatus=aborted`、`followTurnId=091b4738-a537-43f2-a4b9-daf0ed1b3c3d`,`consoleErrorCount=0`、`failureRuntime=null`。 | 已随完整 `verify:local` 通过;不在 P18 SDK lane 内抢修 AgentRuntime / GUI smoke。 | -| `docs/roadmap/agentapp/*` | 客户端路线图需要记录标准资料和 owner 边界。 | 可做 additive 文档同步;避免把 Cloud / LimeCore、外部 package 或 AgentRuntime 实现混进客户端计划。 | -| Lime 工作区自检 | 2026-05-16 07:39 只读复核 `docs/roadmap/agentapp` 与 `src/features/agent-app` 范围仍有 30 个 dirty / untracked 条目,其中 16 个 tracked modified、14 个 untracked;P18 文档收口写集保持在 `docs/roadmap/agentapp/p18-completion-audit.md` 与 `docs/roadmap/agentapp/p18-5-3-package-sdk-migration-plan.md`。 | 后续继续先 `git status --short` 再动手;不把并行实现写集纳入 P18 文档收口。 | +| 输入 / 写集 | 当前观察 | 本轮处理方式 | +| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `/Users/coso/Documents/dev/ai/limecloud/agentapp` | 工作区干净;当前 `HEAD=4bef605 fix: quote mermaid sdk labels`,zh / en 标准资料已覆盖 authoring、client implementation、reference、examples、what-is-agent-app、agent-app-vs-skills-knowledge 与 mini-program analogy,进一步固定 Agent App 是完整应用包、Expert Chat 只是 entry、能力调用必须通过 Capability SDK。 | 作为标准资料只读输入;同步到本审计和后续计划口径,不改标准仓库、不复制标准文档。 | +| `/Users/coso/Documents/dev/ai/limecloud/content-factory-app` | 2026-05-16 07:39 只读复核有 39 个 dirty / untracked 条目,其中 36 个 tracked modified、3 个 untracked;当时 `src/ui/host-bridge.js`、`dist/ui/host-bridge.js`、`src/ui/app.js`、`tests/ui.test.mjs`、`dist/*`、docs 和 model-generation 相关文件仍在并行改动,且私有 bridge marker 仍命中。 | 历史 blocker;已被 10:55 真实 package verify 和 handoff gate `blockers=none / distArtifacts=0` 覆盖。 | +| `src-tauri/*`、`src/lib/api/agentAppRuntime.ts`、`src/features/agent-app/runtime/agentRuntimeCapabilityHost*`、GUI smoke 脚本 | 2026-05-16 07:05 只读复核 owner-sensitive 写集仍有 25 个 dirty / untracked 条目,属于 AgentRuntime / GUI owner 面。 | 不改;P18 只消费已验证 host surface,不抢 Rust / TS facade / GUI smoke。 | +| `smoke:claw-chat-ready-streaming` | 2026-05-16 07:33 只读复核最新 summary 为 `verdict=pass / error=null`;`providerPreference=deepseek`、`modelPreference=deepseek-v4-flash`、`interruptedTurnStatus=aborted`、`followTurnId=091b4738-a537-43f2-a4b9-daf0ed1b3c3d`,`consoleErrorCount=0`、`failureRuntime=null`。 | 已随完整 `verify:local` 通过;不在 P18 SDK lane 内抢修 AgentRuntime / GUI smoke。 | +| `docs/roadmap/agentapp/*` | 客户端路线图需要记录标准资料和 owner 边界。 | 可做 additive 文档同步;避免把 Cloud / LimeCore、外部 package 或 AgentRuntime 实现混进客户端计划。 | +| Lime 工作区自检 | 2026-05-16 07:39 只读复核 `docs/roadmap/agentapp` 与 `src/features/agent-app` 范围仍有 30 个 dirty / untracked 条目,其中 16 个 tracked modified、14 个 untracked;P18 文档收口写集保持在 `docs/roadmap/agentapp/p18-completion-audit.md` 与 `docs/roadmap/agentapp/p18-5-3-package-sdk-migration-plan.md`。 | 后续继续先 `git status --short` 再动手;不把并行实现写集纳入 P18 文档收口。 | 2026-05-16 08:58 当前只读复核: -| 检查项 | 当前事实 | P18 判定 | -|---|---|---| -| 标准与服务端仓库 | `/Users/coso/Documents/dev/ai/limecloud/agentapp` 工作区干净,`HEAD=4bef605`;`/Users/coso/Documents/dev/ai/limecloud/limecore` 工作区干净。 | 标准 / 服务端事实源可读;本轮不改标准仓库或 LimeCore。 | -| 隔壁 Capability 共享输入 | 2026-05-16 只读读取 `docs/roadmap/agentruntime/claw-capability-sharing.md`;该 draft 固定 Chat `@命令`、Agent App `lime.agent.startTask`、Automation job 只是 surface adapter,Claw 能力需收敛到 AgentRuntime capability catalog,首刀已把部分 capability hint 写入现有 Claw `*_skill_launch` metadata。 | 作为 AgentRuntime owner 输入消费;P18 不复制 Claw skill launch,不新增 `content_factory_*` 垂直后端能力,也不把 capability catalog owner 迁入 Agent App SDK。 | -| 外部 package 写集 | `/Users/coso/Documents/dev/ai/limecloud/content-factory-app` 仍有 36 个 tracked modified、3 个 untracked;两个 `npm run dev` 进程的 cwd 都在该仓库。 | owner 仍未稳定;P18.5.3 不接管外部源码、`dist/*`、`tests/*` 或 `package.json`。 | -| 外部私有 bridge marker | `src/ui/host-bridge.js` 仍包含 `pendingRequests / buildMessage / window.parent.postMessage / requestHostBridge / capability:invoke / capability:subscribe / capability:event`,未出现 `createLimeHostBridgeCapabilityInvoker` 或 `createLimeCoreCapabilityAdapters`。 | package-side SDK facade 仍未完成。 | -| 外部 UI 测试 marker | `tests/ui.test.mjs` 仍直接过滤 `message.type === 'capability:invoke'` 并模拟私有 `postMessage` transport。 | 测试仍鼓励私有 transport;迁移时必须改为 SDK invoker / Host Bridge client call log。 | -| Lime 运行面 | Lime 当前仍有 Vite 与 `tauri:dev:headless` 进程在跑。 | 本轮不启停 DevBridge / Vite / Tauri,不跑 GUI smoke,不抢隔壁运行环境。 | -| SDK package 发布边界 | Lime 根 `package.json` 仍是 private 桌面包;`packages/` 仅有 `@limecloud/lime-cli`,未发现正式 `@lime/app-sdk` package。 | 外部迁移若立即 handoff,只能用带退出条件的 package-local shim,不能 import Lime internal path。 | -| 文档轻量验证 | 2026-05-16 08:59 低优先级运行 `npm run harness:doc-freshness`,monitored docs 10 / existing docs 10 / issues 0 / broken markdown links 0 / broken code path refs 0。 | 客户端文档引用未被本轮协作快照破坏;仍不能替代 P18.5.3 外部 package-side SDK facade / verify。 | -| Agent App feature island 越界扫描 | 2026-05-16 09:00 只读运行 `rg -n "SceneApp\|contentEngineering\|sceneapp_\|safeInvoke\|invoke\\(\|new Worker\|Worker\\(" src/features/agent-app \|\| true`,无输出。 | 当前 feature island 未复活旧 SceneApp、未直接 Tauri invoke、未执行 raw Worker;仍不能替代外部 package SDK facade / verify。 | -| Agent App feature island 全量定向测试 | 2026-05-16 10:17 低优先级运行 `npm test -- src/features/agent-app`,36 files / 178 tests passed。 | 当前 Agent App feature island 回归通过,并覆盖 SDK-only public surface 收口;仍不能替代外部 package-side SDK facade / verify。 | -| Lime-side SDK seam 定向测试 | 2026-05-16 09:01 低优先级运行 `npm test -- src/features/agent-app/sdk/hostBridgeClient.test.ts src/features/agent-app/sdk/contentFactorySdkRegression.test.ts src/features/agent-app/index.test.ts`,3 files / 8 tests passed。 | Host Bridge SDK client、内容工厂 SDK regression 与 public SDK export seam 当前仍绿;仍不能替代外部 package-side SDK facade / verify。 | -| Lime typecheck | 2026-05-16 10:55 低优先级运行 `npm run typecheck -- --pretty false`,通过。 | 当前 dirty 工作区的 TypeScript 类型层仍通过,并覆盖内容工厂回归改从 SDK-only public surface 导入后的类型面。 | -| Lime lint | 2026-05-16 11:00 低优先级运行 `npm run lint`,通过。 | 当前 `src` lint 仍通过;脚本变更另由 handoff core test、diff check 与实际 gate 运行覆盖。 | -| Lime contracts | 2026-05-16 10:53 低优先级运行 `npm run test:contracts`,command contracts、harness contracts、modality runtime contracts 与 cleanup report contract 均通过。 | 当前命令 / harness / modality 契约仍通过;不替代外部 package verify,但 package verify 已于 10:55 通过。 | -| Lime app version | 2026-05-16 09:07 低优先级运行 `npm run verify:app-version`,版本一致性检查通过:`1.40.0`。 | 当前版本事实源一致;仍不能替代外部 package-side SDK facade / verify。 | -| 外部 package 只读测试 | 2026-05-16 09:05 在 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app` 低优先级运行 `npm test`,46 tests passed。 | 只证明外部 package 当前业务测试仍绿;测试仍在私有 bridge transport 上通过,不能替代 package-side SDK facade 或会重建 `dist/*` 的 `npm run verify`。 | -| 外部标准只读校验 | 2026-05-16 09:06 在 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app` 低优先级运行 `npm run validate:app && npm run readiness:app`;`validate:app` 为 `ok=true / status=passed`,manifest hash 为 `sha256:6ec3fed5f163739bcf0fd2b845c51a8e10d28aa856e8c6f90259fdab9edd1e48`;`readiness:app` 为 `ok=true / status=needs-setup`。 | 标准 manifest 可读,readiness 正确停在宿主 required 依赖待绑定状态;仍不能替代 package-side SDK facade 或会重建 `dist/*` 的 `npm run verify`。 | -| AgentApp 文档旧口径扫描 | 2026-05-16 09:10 运行 `rg -n "08:30\|171 tests\|35 files / 171" docs/roadmap/agentapp \|\| true`,无输出。 | AgentApp 文档当前验证口径已统一到 09:04 / 09:09;仍不能替代外部 package-side SDK facade / verify。 | -| 垂直后端能力越界扫描 | 2026-05-16 09:11 只读扫描 `src-tauri / src / scripts` 中 `content_factory_* command / skill_launch` 形态;命中项集中在 `src-tauri/src/commands/agent_app_runtime_cmd.rs` 的 `is_content_factory_runtime_task`、output contract helper 与测试,用于避免复合内容工厂任务被强制提升为单一 Claw skill launch。 | 当前未发现 P18 新增垂直 `content_factory_*` 后端 command;该 AgentRuntime helper 属隔壁 runtime owner 面,P18 只消费、不接管。 | -| 外部 package SDK facade marker 复核 | 2026-05-16 09:22 只读复核 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app`;`src/ui/host-bridge.js` 仍包含 `pendingRequests / buildMessage / window.parent.postMessage / requestHostBridge / capability:invoke / capability:subscribe / capability:event`,`tests/ui.test.mjs` 仍直接过滤 `message.type === 'capability:invoke'`。 | P18.5.3 唯一主阻塞仍未解除;外部 package 尚未成为 SDK facade consumer。 | -| Handoff 确认状态 | 2026-05-16 09:26 已进入需显式确认状态:没有用户或外部 package owner 明确“接管外部 package 最小写集”前,不修改 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app/src/ui/host-bridge.js`、`tests/ui.test.mjs`、`package.json` 或 `dist/*`。 | 当前只能继续只读复核 / 文档审计;不能实施 P18.5.3 package-side SDK facade。 | -| 运行面复核 | 2026-05-16 09:28 只读复核进程,两个 `npm run dev` 的 cwd 都在 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app`;Lime 侧仍有 Vite 与 `tauri:dev:headless` 进程。 | 外部 package 与 Lime GUI / Tauri 运行面仍在并行;不能抢占或启停这些进程。 | -| 09:39 再复核 | 外部 package 当时有 36 个 tracked modified、3 个 untracked;`src/ui/host-bridge.js` / `tests/ui.test.mjs` marker 命中手写 transport;外部 `npm test` 重新通过 46 tests;Lime-side SDK seam 最小集 `hostBridgeClient.test.ts / contentFactorySdkRegression.test.ts / index.test.ts` 重新通过 3 files / 8 tests;`src/features/agent-app` 越界扫描无输出。 | 历史 blocker;已被 10:55 真实 package verify 覆盖。 | -| 09:48 机械 gate | 新增 `scripts/agent-app-package-handoff-check.mjs` / `scripts/lib/agent-app-package-handoff-core.mjs` / `scripts/lib/agent-app-package-handoff-core.test.ts`,用只读方式把 dirty 计数、私有 bridge marker、SDK facade marker、`scripts/build.mjs` 与高风险脚本转成 `ready / needs_handoff / blocked` 判定;当前外部 package 输出 `status=blocked`,并命中 `build / verify / e2e:user-flow / e2e:user-flow:fake-model` 高风险脚本。 | P18.5.3 后续不再只靠人工 grep;但当前 gate 结果仍证明外部 package 未完成 SDK facade。 | -| 10:17 外部 gate 复核 | 运行 `node scripts/agent-app-package-handoff-check.mjs --package-dir /Users/coso/Documents/dev/ai/limecloud/content-factory-app --check`,退出码 1;输出为 `status=blocked`、dirty `tracked=36 / untracked=3`、hostBridgeSdk `none`,私有 bridge marker 与 highRiskScripts 均未消失。 | 历史 blocker;已被 10:55 真实 package verify 覆盖。 | -| 10:33 外部 gate 复核 | 运行 `node scripts/agent-app-package-handoff-check.mjs --package-dir /Users/coso/Documents/dev/ai/limecloud/content-factory-app`,输出 `status=needs_handoff`、dirty `tracked=36 / untracked=4 / total=40`、`hostBridgePrivate=none`、`uiTestPrivate=none`、`hostBridgeSdk=createLimeHostBridgeCapabilityInvoker:2, createLimeCoreCapabilityAdapters:2`,blockers 为 `none`。 | source-side facade blocker 已解除;当时主缺口切换为 owner handoff / dist verify;已在 10:55 收口。 | -| 10:41 不重建 dist 验证 | Lime repo 低优先级运行 SDK seam 最小集:4 files / 11 tests passed;外部 package 低优先级运行 `npm test`:46 tests passed;`validate:app` passed;`readiness:app` needs-setup。 | 当前 source-side facade 与标准 manifest 仍绿;仍不能替代 `npm run verify`,因为 verify 会先 build 并重建 `dist/*`。 | -| 10:45 dist 重建前预审 | 只读解析外部 `scripts/build.mjs`,确认 build 会先删除 `dist` 再复制 `src/*`;hash 对比发现 `src/ui/host-bridge.js` 与 `dist/ui/host-bridge.js` 不一致,且 `src/ui/lime-app-sdk.js` 尚无 `dist/ui/lime-app-sdk.js`。 | 安装产物缺口已明确:source-side facade 尚未同步到 dist;仍需 owner handoff 后重建 dist 或记录退出条件。 | -| 10:47 隔离 verify 演练 | 复制外部 package 当前工作区到 `/tmp/limecloud-content-factory-verify.uots6S/content-factory-app`,用 sibling symlink 保持 `../agentapp` 可用,并在临时副本运行 `npm run verify`;build、46 tests、validate、readiness 全部通过,readiness 仍为预期 `needs-setup`。 | 证明当前源码若允许重建 dist,大概率可通过 package verify;仍不能替代真实 package 写集验收,因为真实 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app/dist/*` 未被更新。 | -| 10:55 真实 package verify | 用户确认后,在真实 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app` 运行 `nice -n 10 npm run verify`:build 通过,46 tests passed,`validate:app` passed,`readiness:app` needs-setup;随后 handoff gate 显示 `distArtifacts=diff:0,missing:0,extra:0,total:0`。 | P18.5.3 的 package verify / dist 验收完成;gate 仍为 `needs_handoff` 是因为工作区 dirty 与高风险脚本 warning,不是 SDK facade blocker。 | +| 检查项 | 当前事实 | P18 判定 | +| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 标准与服务端仓库 | `/Users/coso/Documents/dev/ai/limecloud/agentapp` 工作区干净,`HEAD=4bef605`;`/Users/coso/Documents/dev/ai/limecloud/limecore` 工作区干净。 | 标准 / 服务端事实源可读;本轮不改标准仓库或 LimeCore。 | +| 隔壁 Capability 共享输入 | 2026-05-16 只读读取 `docs/roadmap/agentruntime/claw-capability-sharing.md`;该 draft 固定 Chat `@命令`、Agent App `lime.agent.startTask`、Automation job 只是 surface adapter,Claw 能力需收敛到 AgentRuntime capability catalog,首刀已把部分 capability hint 写入现有 Claw `*_skill_launch` metadata。 | 作为 AgentRuntime owner 输入消费;P18 不复制 Claw skill launch,不新增 `content_factory_*` 垂直后端能力,也不把 capability catalog owner 迁入 Agent App SDK。 | +| 外部 package 写集 | `/Users/coso/Documents/dev/ai/limecloud/content-factory-app` 仍有 36 个 tracked modified、3 个 untracked;两个 `npm run dev` 进程的 cwd 都在该仓库。 | owner 仍未稳定;P18.5.3 不接管外部源码、`dist/*`、`tests/*` 或 `package.json`。 | +| 外部私有 bridge marker | `src/ui/host-bridge.js` 仍包含 `pendingRequests / buildMessage / window.parent.postMessage / requestHostBridge / capability:invoke / capability:subscribe / capability:event`,未出现 `createLimeHostBridgeCapabilityInvoker` 或 `createLimeCoreCapabilityAdapters`。 | package-side SDK facade 仍未完成。 | +| 外部 UI 测试 marker | `tests/ui.test.mjs` 仍直接过滤 `message.type === 'capability:invoke'` 并模拟私有 `postMessage` transport。 | 测试仍鼓励私有 transport;迁移时必须改为 SDK invoker / Host Bridge client call log。 | +| Lime 运行面 | Lime 当前仍有 Vite 与 `tauri:dev:headless` 进程在跑。 | 本轮不启停 DevBridge / Vite / Tauri,不跑 GUI smoke,不抢隔壁运行环境。 | +| SDK package 发布边界 | Lime 根 `package.json` 仍是 private 桌面包;`packages/` 仅有 `@limecloud/lime-cli`,未发现正式 `@lime/app-sdk` package。 | 外部迁移若立即 handoff,只能用带退出条件的 package-local shim,不能 import Lime internal path。 | +| 文档轻量验证 | 2026-05-16 08:59 低优先级运行 `npm run harness:doc-freshness`,monitored docs 10 / existing docs 10 / issues 0 / broken markdown links 0 / broken code path refs 0。 | 客户端文档引用未被本轮协作快照破坏;仍不能替代 P18.5.3 外部 package-side SDK facade / verify。 | +| Agent App feature island 越界扫描 | 2026-05-16 09:00 只读运行 `rg -n "SceneApp\|contentEngineering\|sceneapp_\|safeInvoke\|invoke\\(\|new Worker\|Worker\\(" src/features/agent-app \|\| true`,无输出。 | 当前 feature island 未复活旧 SceneApp、未直接 Tauri invoke、未执行 raw Worker;仍不能替代外部 package SDK facade / verify。 | +| Agent App feature island 全量定向测试 | 2026-05-16 10:17 低优先级运行 `npm test -- src/features/agent-app`,36 files / 178 tests passed。 | 当前 Agent App feature island 回归通过,并覆盖 SDK-only public surface 收口;仍不能替代外部 package-side SDK facade / verify。 | +| Lime-side SDK seam 定向测试 | 2026-05-16 09:01 低优先级运行 `npm test -- src/features/agent-app/sdk/hostBridgeClient.test.ts src/features/agent-app/sdk/contentFactorySdkRegression.test.ts src/features/agent-app/index.test.ts`,3 files / 8 tests passed。 | Host Bridge SDK client、内容工厂 SDK regression 与 public SDK export seam 当前仍绿;仍不能替代外部 package-side SDK facade / verify。 | +| Lime typecheck | 2026-05-16 10:55 低优先级运行 `npm run typecheck -- --pretty false`,通过。 | 当前 dirty 工作区的 TypeScript 类型层仍通过,并覆盖内容工厂回归改从 SDK-only public surface 导入后的类型面。 | +| Lime lint | 2026-05-16 11:00 低优先级运行 `npm run lint`,通过。 | 当前 `src` lint 仍通过;脚本变更另由 handoff core test、diff check 与实际 gate 运行覆盖。 | +| Lime contracts | 2026-05-16 10:53 低优先级运行 `npm run test:contracts`,command contracts、harness contracts、modality runtime contracts 与 cleanup report contract 均通过。 | 当前命令 / harness / modality 契约仍通过;不替代外部 package verify,但 package verify 已于 10:55 通过。 | +| Lime app version | 2026-05-16 09:07 低优先级运行 `npm run verify:app-version`,版本一致性检查通过:`1.40.0`。 | 当前版本事实源一致;仍不能替代外部 package-side SDK facade / verify。 | +| 外部 package 只读测试 | 2026-05-16 09:05 在 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app` 低优先级运行 `npm test`,46 tests passed。 | 只证明外部 package 当前业务测试仍绿;测试仍在私有 bridge transport 上通过,不能替代 package-side SDK facade 或会重建 `dist/*` 的 `npm run verify`。 | +| 外部标准只读校验 | 2026-05-16 09:06 在 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app` 低优先级运行 `npm run validate:app && npm run readiness:app`;`validate:app` 为 `ok=true / status=passed`,manifest hash 为 `sha256:6ec3fed5f163739bcf0fd2b845c51a8e10d28aa856e8c6f90259fdab9edd1e48`;`readiness:app` 为 `ok=true / status=needs-setup`。 | 标准 manifest 可读,readiness 正确停在宿主 required 依赖待绑定状态;仍不能替代 package-side SDK facade 或会重建 `dist/*` 的 `npm run verify`。 | +| AgentApp 文档旧口径扫描 | 2026-05-16 09:10 运行 `rg -n "08:30\|171 tests\|35 files / 171" docs/roadmap/agentapp \|\| true`,无输出。 | AgentApp 文档当前验证口径已统一到 09:04 / 09:09;仍不能替代外部 package-side SDK facade / verify。 | +| 垂直后端能力越界扫描 | 2026-05-16 09:11 只读扫描 `src-tauri / src / scripts` 中 `content_factory_* command / skill_launch` 形态;命中项集中在 `src-tauri/src/commands/agent_app_runtime_cmd.rs` 的 `is_content_factory_runtime_task`、output contract helper 与测试,用于避免复合内容工厂任务被强制提升为单一 Claw skill launch。 | 当前未发现 P18 新增垂直 `content_factory_*` 后端 command;该 AgentRuntime helper 属隔壁 runtime owner 面,P18 只消费、不接管。 | +| 外部 package SDK facade marker 复核 | 2026-05-16 09:22 只读复核 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app`;`src/ui/host-bridge.js` 仍包含 `pendingRequests / buildMessage / window.parent.postMessage / requestHostBridge / capability:invoke / capability:subscribe / capability:event`,`tests/ui.test.mjs` 仍直接过滤 `message.type === 'capability:invoke'`。 | P18.5.3 唯一主阻塞仍未解除;外部 package 尚未成为 SDK facade consumer。 | +| Handoff 确认状态 | 2026-05-16 09:26 已进入需显式确认状态:没有用户或外部 package owner 明确“接管外部 package 最小写集”前,不修改 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app/src/ui/host-bridge.js`、`tests/ui.test.mjs`、`package.json` 或 `dist/*`。 | 当前只能继续只读复核 / 文档审计;不能实施 P18.5.3 package-side SDK facade。 | +| 运行面复核 | 2026-05-16 09:28 只读复核进程,两个 `npm run dev` 的 cwd 都在 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app`;Lime 侧仍有 Vite 与 `tauri:dev:headless` 进程。 | 外部 package 与 Lime GUI / Tauri 运行面仍在并行;不能抢占或启停这些进程。 | +| 09:39 再复核 | 外部 package 当时有 36 个 tracked modified、3 个 untracked;`src/ui/host-bridge.js` / `tests/ui.test.mjs` marker 命中手写 transport;外部 `npm test` 重新通过 46 tests;Lime-side SDK seam 最小集 `hostBridgeClient.test.ts / contentFactorySdkRegression.test.ts / index.test.ts` 重新通过 3 files / 8 tests;`src/features/agent-app` 越界扫描无输出。 | 历史 blocker;已被 10:55 真实 package verify 覆盖。 | +| 09:48 机械 gate | 新增 `scripts/agent-app-package-handoff-check.mjs` / `scripts/lib/agent-app-package-handoff-core.mjs` / `scripts/lib/agent-app-package-handoff-core.test.ts`,用只读方式把 dirty 计数、私有 bridge marker、SDK facade marker、`scripts/build.mjs` 与高风险脚本转成 `ready / needs_handoff / blocked` 判定;当前外部 package 输出 `status=blocked`,并命中 `build / verify / e2e:user-flow / e2e:user-flow:fake-model` 高风险脚本。 | P18.5.3 后续不再只靠人工 grep;但当前 gate 结果仍证明外部 package 未完成 SDK facade。 | +| 10:17 外部 gate 复核 | 运行 `node scripts/agent-app-package-handoff-check.mjs --package-dir /Users/coso/Documents/dev/ai/limecloud/content-factory-app --check`,退出码 1;输出为 `status=blocked`、dirty `tracked=36 / untracked=3`、hostBridgeSdk `none`,私有 bridge marker 与 highRiskScripts 均未消失。 | 历史 blocker;已被 10:55 真实 package verify 覆盖。 | +| 10:33 外部 gate 复核 | 运行 `node scripts/agent-app-package-handoff-check.mjs --package-dir /Users/coso/Documents/dev/ai/limecloud/content-factory-app`,输出 `status=needs_handoff`、dirty `tracked=36 / untracked=4 / total=40`、`hostBridgePrivate=none`、`uiTestPrivate=none`、`hostBridgeSdk=createLimeHostBridgeCapabilityInvoker:2, createLimeCoreCapabilityAdapters:2`,blockers 为 `none`。 | source-side facade blocker 已解除;当时主缺口切换为 owner handoff / dist verify;已在 10:55 收口。 | +| 10:41 不重建 dist 验证 | Lime repo 低优先级运行 SDK seam 最小集:4 files / 11 tests passed;外部 package 低优先级运行 `npm test`:46 tests passed;`validate:app` passed;`readiness:app` needs-setup。 | 当前 source-side facade 与标准 manifest 仍绿;仍不能替代 `npm run verify`,因为 verify 会先 build 并重建 `dist/*`。 | +| 10:45 dist 重建前预审 | 只读解析外部 `scripts/build.mjs`,确认 build 会先删除 `dist` 再复制 `src/*`;hash 对比发现 `src/ui/host-bridge.js` 与 `dist/ui/host-bridge.js` 不一致,且 `src/ui/lime-app-sdk.js` 尚无 `dist/ui/lime-app-sdk.js`。 | 安装产物缺口已明确:source-side facade 尚未同步到 dist;仍需 owner handoff 后重建 dist 或记录退出条件。 | +| 10:47 隔离 verify 演练 | 复制外部 package 当前工作区到 `/tmp/limecloud-content-factory-verify.uots6S/content-factory-app`,用 sibling symlink 保持 `../agentapp` 可用,并在临时副本运行 `npm run verify`;build、46 tests、validate、readiness 全部通过,readiness 仍为预期 `needs-setup`。 | 证明当前源码若允许重建 dist,大概率可通过 package verify;仍不能替代真实 package 写集验收,因为真实 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app/dist/*` 未被更新。 | +| 10:55 真实 package verify | 用户确认后,在真实 `/Users/coso/Documents/dev/ai/limecloud/content-factory-app` 运行 `nice -n 10 npm run verify`:build 通过,46 tests passed,`validate:app` passed,`readiness:app` needs-setup;随后 handoff gate 显示 `distArtifacts=diff:0,missing:0,extra:0,total:0`。 | P18.5.3 的 package verify / dist 验收完成;gate 仍为 `needs_handoff` 是因为工作区 dirty 与高风险脚本 warning,不是 SDK facade blocker。 | 冲突规约: @@ -180,15 +185,15 @@ nice -n 10 npm run harness:doc-freshness 4. 本机磁盘空间已恢复到可跑 Rust 测试;不在未确认时删除 `src-tauri/target` 或其他构建缓存。`smoke:claw-chat-ready-streaming` 的 06:23 历史失败已在 07:33 完整 `verify:local` 中转为 pass 证据。 5. P18 后续只在通用 SDK contract 存在缺口时补 `src/features/agent-app/sdk/*`,不新增垂直 `content_factory_*` host command 或第二套 bridge。 -因此当前优先级调整为:不再把“外部 source-side facade、package verify、dist 同步”作为 P18 blocker;下一刀只剩 owner handoff、提交边界与是否归档 P18 的工程收口。完整 `verify:local` 已通过,不再把全局 GUI smoke 作为当前阻塞。 +因此当前优先级调整为:不再把“外部 source-side facade、package verify、dist 同步”作为 P18 blocker;P18.7-B 也已解除 `manifestVersion: 0.6.0` reference 阻塞。下一刀回到 P18.7-C Host capability discovery。完整 `verify:local` 仍需在并行写集稳定后再作为整体验收复跑,本轮定向验证已经覆盖 v0.6 兼容风险。 ## 下一刀 -1. 外部 `content-factory-app` owner 接收当前 dirty 写集,尤其是 `src/ui/host-bridge.js`、`src/ui/lime-app-sdk.js`、`tests/ui.test.mjs` 与 `dist/ui/*`。 -2. Lime 侧 owner 接收新增 SDK / handoff gate 脚本与 P18 roadmap 文档写集。 -3. 如后续还有外部 package 变更,先复跑 handoff gate;只有 `blockers=none` 且必要验证通过后再进入 P19。 -4. P18 归档后再启动 P19 raw worker、marketplace 扩展或内容工厂后端产品化;不要在 P18 写集中混入下一阶段。 +1. P18.7-C:实现 `lime.capabilities` Host discovery surface,让 App 读取 catalog/profile 摘要、实现状态、stage、owner 和 unavailable reason。 +2. P18.7-D:接 `lime.usage / lime.models / lime.skills / lime.memory / lime.context` 到 AgentRuntime facts,而不是 App 自己估算模型、token 或 skill 使用。 +3. P18.7-E:接 `lime.search / lime.browser / lime.documents / lime.media / lime.mcp / lime.terminal / lime.connectors` 到 ToolRuntime / Desktop Host policy。 +4. P18.7-F:复核内容工厂真实闭环,确保生成过程、思考、执行、Skills、模型、Token、artifact、evidence 都留在 App 内且来自 Host projection。 ## 完成判定 -P18 当前满足功能完成判定:外部 `content-factory-app` package-side SDK facade 已完成,真实 package verify 已通过,Lime formal runtime / SDK regression 有迁移后新证据,并且没有引入私有 bridge、raw worker 或第二套 runtime。当前不等于“git 可提交完成”,因为外部 package 与 Lime 工作区仍 dirty,需要 owner 决定提交 / 推送边界。 +P18.1-P18.6 和 P18.7-A/B 满足各自功能完成判定:外部 `content-factory-app` package-side SDK facade 已完成,真实 package verify 已通过,Lime formal runtime / SDK regression 有迁移后新证据,且 Agent App v0.6 reference compatibility 已复绿。P18.7 整体尚未完成,因为 Host discovery、AgentRuntime resource、Tool/Integration 和内容工厂真实闭环仍未全部接线;同时外部 package 与 Lime 工作区仍 dirty,需要 owner 决定提交 / 推送边界。 diff --git a/docs/roadmap/agentapp/p18-typed-capability-sdk-gate.md b/docs/roadmap/agentapp/p18-typed-capability-sdk-gate.md index feddb847e..14e910a2e 100644 --- a/docs/roadmap/agentapp/p18-typed-capability-sdk-gate.md +++ b/docs/roadmap/agentapp/p18-typed-capability-sdk-gate.md @@ -4,27 +4,31 @@ 状态:P18.0 文档计划已完成;P18.1 SDK facade / stable error / mock host 已完成最小代码契约;P18.2 Host Bridge typed router / stable error response 已完成;P18.3 Core capability adapters 已完成;P18.4 App-scoped Agent task SDK facade 已完成;P18.4-H AgentRuntime handoff gate 已完成;P18.5.1 Lime-side 内容工厂 SDK 回归已完成;P18.5.2 package-side read-only tests 已通过;P18.5-S Host Bridge SDK client 已完成 capability invoke、subscription、Host action 和 Host event contract;P18.5.3 package-side SDK facade / verify / dist 同步已完成;P18.6 Raw Worker 前 Gate 已完成;P17.5 Formal entry GUI smoke 已通过;完整 `verify:local` 已于 2026-05-16 07:33 通过,2026-05-16 10:53 SDK seam / handoff core 定向测试 5 files / 17 tests passed,`typecheck`、`test:contracts` 与 `lint` 当前会话复核通过;P18 功能完成,剩余 owner handoff / git 写集收口。 +## P18.7 重新打开原因 + +2026-05-16 追加:用户要求“将 Lime 所有功能都抽象出来”,因此 P18 不再只以 typed SDK gate 完成为终点。新的 current 路线图是 [P18.7 Full Lime Capability Surface](./p18-7-full-lime-capability-surface.md):先把全量 `lime.*` 能力、App / Lime 主 App 边界、v0.6 标准兼容、Host discovery 与后端接线顺序落成文档事实源,再继续代码。原 P18.1-P18.6 仍是已完成基础层,不代表整体目标完成。 + ## 协作分工 P17.5 正式入口 smoke 已收口;P18 代码实施继续和 AgentRuntime / GUI 验证任务分工,避免抢同一运行环境或改同一底层投影。 -| 分工 | 负责范围 | 暂不触碰 | -|---|---|---| -| P17.5 / 已收口 | 正式 `agent-apps` smoke、`scripts/agent-apps-smoke.mjs`、P17.5 evidence summary。 | 不把通过 smoke 扩大解释为 marketplace / 真实 delete-data / Cloud 管理台。 | -| AgentRuntime / 隔壁任务 | `AgentRuntimeThreadReadModel`、`agent_app_runtime_*` facade、artifact / evidence / handoff 投影、`artifact:created` refs、跨刷新 task 恢复、Host response 回写;`docs/roadmap/agentruntime/claw-capability-sharing.md` 进一步把 Claw `@` 能力收敛到 AgentRuntime capability catalog。 | 不改 Agent App SDK 路线图,不由 P18 复制 runtime read model;push subscribe、workspace patch producer、capability catalog service、Claw capability catalog 和真实桌面 GUI smoke 仍由 AgentRuntime 任务收口。 | -| P18 / 完成候选 | 上游 Agent App v0.4 Host Bridge 标准对齐、Typed Capability SDK gate、App-scoped Agent task 契约。P18.1 已落 SDK 类型、stable error、mock host 和 contract seam;P18.2 已收敛 Host Bridge typed router;P18.3 已完成 core adapters;P18.4 已完成 `lime.agent` SDK facade;P18.4-H 已完成 handoff gate;P18.5.1 已完成 Lime-side 内容工厂 SDK 回归;P18.5.2 已完成 package-side read-only tests;P18.5-S 已补 Host Bridge SDK client;P18.5.3 已完成真实 package verify 和 dist 同步;P18.6 已完成 raw worker 前 gate。 | 不启停隔壁 Tauri / Vite / DevBridge,不改 `src-tauri/*` / runtime facade,不改 `src/features/agent-app/runtime/agentRuntimeCapabilityHost*`,不进入 raw worker;后续只做 owner handoff / 提交边界,不再扩 P18 功能面。 | +| 分工 | 负责范围 | 暂不触碰 | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| P17.5 / 已收口 | 正式 `agent-apps` smoke、`scripts/agent-apps-smoke.mjs`、P17.5 evidence summary。 | 不把通过 smoke 扩大解释为 marketplace / 真实 delete-data / Cloud 管理台。 | +| AgentRuntime / 隔壁任务 | `AgentRuntimeThreadReadModel`、`agent_app_runtime_*` facade、artifact / evidence / handoff 投影、`artifact:created` refs、跨刷新 task 恢复、Host response 回写;`docs/roadmap/agentruntime/claw-capability-sharing.md` 进一步把 Claw `@` 能力收敛到 AgentRuntime capability catalog。 | 不改 Agent App SDK 路线图,不由 P18 复制 runtime read model;push subscribe、workspace patch producer、capability catalog service、Claw capability catalog 和真实桌面 GUI smoke 仍由 AgentRuntime 任务收口。 | +| P18 / 完成候选 | 上游 Agent App v0.4 Host Bridge 标准对齐、Typed Capability SDK gate、App-scoped Agent task 契约。P18.1 已落 SDK 类型、stable error、mock host 和 contract seam;P18.2 已收敛 Host Bridge typed router;P18.3 已完成 core adapters;P18.4 已完成 `lime.agent` SDK facade;P18.4-H 已完成 handoff gate;P18.5.1 已完成 Lime-side 内容工厂 SDK 回归;P18.5.2 已完成 package-side read-only tests;P18.5-S 已补 Host Bridge SDK client;P18.5.3 已完成真实 package verify 和 dist 同步;P18.6 已完成 raw worker 前 gate。 | 不启停隔壁 Tauri / Vite / DevBridge,不改 `src-tauri/*` / runtime facade,不改 `src/features/agent-app/runtime/agentRuntimeCapabilityHost*`,不进入 raw worker;后续只做 owner handoff / 提交边界,不再扩 P18 功能面。 | 收敛规则:P17.5 已解除正式入口 smoke 阻塞;P18 代码只从 SDK contract 最小面开始,不把 SDK gate 扩大成垂直内容系统。 ## 协作接口边界 -| 接口面 | 隔壁 AgentRuntime 提供 | P18 消费方式 | 防打架规则 | -|---|---|---|---| -| `lime.agent` task facade | `start / stream / get / cancel / retry / submitHostResponse / listTasks` 到 `agent_app_runtime_*`,并映射 `sessionId / turnId / requestId`。 | P18.1 已定义 SDK 类型和 mock 行为;P18.4 已包装成 typed adapter;P18.4-H 已消费隔壁运行证据。 | P18 不改 Rust command、不改 `src/lib/api/agentAppRuntime.ts`。 | -| task event projection | queued / progress / missing context / review / tool call / `artifact:created` / evidence / outcome / incident。 | SDK 固定 event union、stable error 和 mock fixtures;P18.4-H 已完成 handoff 校验,push subscribe 继续归 AgentRuntime owner。 | 不在 SDK 层重建 read model,不把轮询实现写进业务 App。 | -| storage / artifact / evidence write-back | 内容工厂已用声明类型写回 `lime.storage / lime.artifacts / lime.evidence`,AgentRuntime 负责事实投影。 | P18.3 只包装 typed adapter,自动带 provenance。 | 不新增第二套 artifact / evidence store,不写垂直 `content_factory_*` adapter。 | -| structured workspace patch | 内容工厂 App 已能消费 `workspacePatch / contentFactoryWorkspacePatch`;后端 producer contract 仍是 AgentRuntime 缺口。 | P18 先把 patch 作为可选 typed artifact/event,不把它当 P18.1 完成条件。 | 不由 SDK mock 伪造生产级 patch 成功;真实 producer 缺口回挂 AgentRuntime。 | -| capability catalog | `docs/roadmap/agentruntime/claw-capability-sharing.md` 规定 Chat `@命令`、Agent App `lime.agent.startTask`、Automation job 都只是 surface adapter;首批 capability hints 已可映射 Claw `*_skill_launch` metadata,独立 catalog service 未完成。 | P18 先在 SDK 层定义 `lime.tools / lime.knowledge / lime.agent` 能力声明与错误语义;把 capability sharing 作为 AgentRuntime owner 输入消费。 | 不复制 Claw `*_skill_launch.rs`,不把 capability hint 内置成 App 专用逻辑,不新增 `content_factory_*` 垂直后端能力。 | +| 接口面 | 隔壁 AgentRuntime 提供 | P18 消费方式 | 防打架规则 | +| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `lime.agent` task facade | `start / stream / get / cancel / retry / submitHostResponse / listTasks` 到 `agent_app_runtime_*`,并映射 `sessionId / turnId / requestId`。 | P18.1 已定义 SDK 类型和 mock 行为;P18.4 已包装成 typed adapter;P18.4-H 已消费隔壁运行证据。 | P18 不改 Rust command、不改 `src/lib/api/agentAppRuntime.ts`。 | +| task event projection | queued / progress / missing context / review / tool call / `artifact:created` / evidence / outcome / incident。 | SDK 固定 event union、stable error 和 mock fixtures;P18.4-H 已完成 handoff 校验,push subscribe 继续归 AgentRuntime owner。 | 不在 SDK 层重建 read model,不把轮询实现写进业务 App。 | +| storage / artifact / evidence write-back | 内容工厂已用声明类型写回 `lime.storage / lime.artifacts / lime.evidence`,AgentRuntime 负责事实投影。 | P18.3 只包装 typed adapter,自动带 provenance。 | 不新增第二套 artifact / evidence store,不写垂直 `content_factory_*` adapter。 | +| structured workspace patch | 内容工厂 App 已能消费 `workspacePatch / contentFactoryWorkspacePatch`;后端 producer contract 仍是 AgentRuntime 缺口。 | P18 先把 patch 作为可选 typed artifact/event,不把它当 P18.1 完成条件。 | 不由 SDK mock 伪造生产级 patch 成功;真实 producer 缺口回挂 AgentRuntime。 | +| capability catalog | `docs/roadmap/agentruntime/claw-capability-sharing.md` 规定 Chat `@命令`、Agent App `lime.agent.startTask`、Automation job 都只是 surface adapter;首批 capability hints 已可映射 Claw `*_skill_launch` metadata,独立 catalog service 未完成。 | P18 先在 SDK 层定义 `lime.tools / lime.knowledge / lime.agent` 能力声明与错误语义;把 capability sharing 作为 AgentRuntime owner 输入消费。 | 不复制 Claw `*_skill_launch.rs`,不把 capability hint 内置成 App 专用逻辑,不新增 `content_factory_*` 垂直后端能力。 | ## 2026-05-16 隔壁 AgentRuntime 输入 @@ -98,32 +102,32 @@ flowchart TD ## 边界表 -| 层 | 负责 | 不负责 | -|---|---|---| -| `@lime/app-sdk` | TypeScript facade、调用参数类型、稳定错误码、mock host。 | 不 import Lime internal path,不执行模型 / 工具。 | -| Host Bridge v1 | ready、snapshot、theme、visibility、toast、navigate、download、capability invoke 传输。 | 不绕过 readiness / permission / policy,不返回假成功。 | -| Capability Router | 校验 appId、entryKey、origin、source、capability allowlist、requestId。 | 不拥有业务 UI,不做垂直业务逻辑。 | -| AgentRuntime Surface | App-scoped task、stream、cancel、retry、trace、evidence。 | 不把 Claw Chat 复制一套,不让 App 直接拿模型 API。 | -| App runtime package | UI、workflow、storage schema、业务状态和结果确认。 | 不保存 secret 明文,不访问宿主文件系统 / DB / Tauri API。 | -| Lime Cloud / LimeCore | catalog、release、tenant enablement、license、policy defaults。 | 不默认运行 Agent,不渲染 App UI,不接管本地 storage。 | +| 层 | 负责 | 不负责 | +| --------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------- | +| `@lime/app-sdk` | TypeScript facade、调用参数类型、稳定错误码、mock host。 | 不 import Lime internal path,不执行模型 / 工具。 | +| Host Bridge v1 | ready、snapshot、theme、visibility、toast、navigate、download、capability invoke 传输。 | 不绕过 readiness / permission / policy,不返回假成功。 | +| Capability Router | 校验 appId、entryKey、origin、source、capability allowlist、requestId。 | 不拥有业务 UI,不做垂直业务逻辑。 | +| AgentRuntime Surface | App-scoped task、stream、cancel、retry、trace、evidence。 | 不把 Claw Chat 复制一套,不让 App 直接拿模型 API。 | +| App runtime package | UI、workflow、storage schema、业务状态和结果确认。 | 不保存 secret 明文,不访问宿主文件系统 / DB / Tauri API。 | +| Lime Cloud / LimeCore | catalog、release、tenant enablement、license、policy defaults。 | 不默认运行 Agent,不渲染 App UI,不接管本地 storage。 | ## Typed Capability Matrix P18 的最小 SDK 不追求能力全覆盖,而是先固定调用语义、错误语义和 mock 语义。 -| Capability | 最小调用 | P18 口径 | -|---|---|---| -| `lime.ui` | `toast`、`navigate`、`openExternal`、`download`、`getSnapshot` | 走 Host Bridge action;外链 / 下载必须校验协议、origin、权限。 | -| `lime.storage` | `table.get`、`table.insert`、`table.update`、`table.query` | 只写 App namespace;自动附加 provenance;schema 不匹配返回 stable error。 | -| `lime.files` | `pick`、`readRef` | 只接收 host file ref,不暴露 raw path。 | -| `lime.agent` | `startTask`、`streamTask`、`getTask`、`cancelTask`、`retryTask`、`submitHostResponse`、`listTasks` | 映射到 AgentRuntime Surface;不跳通用 Chat;P18.4 已补 typed adapter,P18.4-H 已对齐 handoff 运行证据。 | -| `lime.knowledge` | `search`、`bindStatus` | 只能访问 manifest / setup 声明的 Knowledge binding。 | -| `lime.tools` | `invoke`、`getProgress` | 通过 Tool Broker / ToolHub;policy 和 secret handle 由 Host 管。 | -| `lime.artifacts` | `create`、`open`、`export` | artifact 带 appId、entryKey、packageHash、manifestHash。 | -| `lime.workflow` | `start`、`checkpoint`、`awaitHuman` | P18 仍是 typed workflow;raw worker 进入后续 gate。 | -| `lime.policy` | `check`、`requestPermission` | UI prompt 不是最终授权;bridge 层必须强制。 | -| `lime.secrets` | `getRef`、`requestBinding` | App 只能拿 scoped handle,不能拿 secret value。 | -| `lime.evidence` | `record`、`linkArtifact` | 记录 task、tool、knowledge、artifact、eval 和 cleanup provenance。 | +| Capability | 最小调用 | P18 口径 | +| ---------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| `lime.ui` | `toast`、`navigate`、`openExternal`、`download`、`getSnapshot` | 走 Host Bridge action;外链 / 下载必须校验协议、origin、权限。 | +| `lime.storage` | `table.get`、`table.insert`、`table.update`、`table.query` | 只写 App namespace;自动附加 provenance;schema 不匹配返回 stable error。 | +| `lime.files` | `pick`、`readRef` | 只接收 host file ref,不暴露 raw path。 | +| `lime.agent` | `startTask`、`streamTask`、`getTask`、`cancelTask`、`retryTask`、`submitHostResponse`、`listTasks` | 映射到 AgentRuntime Surface;不跳通用 Chat;P18.4 已补 typed adapter,P18.4-H 已对齐 handoff 运行证据。 | +| `lime.knowledge` | `search`、`bindStatus` | 只能访问 manifest / setup 声明的 Knowledge binding。 | +| `lime.tools` | `invoke`、`getProgress` | 通过 Tool Broker / ToolHub;policy 和 secret handle 由 Host 管。 | +| `lime.artifacts` | `create`、`open`、`export` | artifact 带 appId、entryKey、packageHash、manifestHash。 | +| `lime.workflow` | `start`、`checkpoint`、`awaitHuman` | P18 仍是 typed workflow;raw worker 进入后续 gate。 | +| `lime.policy` | `check`、`requestPermission` | UI prompt 不是最终授权;bridge 层必须强制。 | +| `lime.secrets` | `getRef`、`requestBinding` | App 只能拿 scoped handle,不能拿 secret value。 | +| `lime.evidence` | `record`、`linkArtifact` | 记录 task、tool、knowledge、artifact、eval 和 cleanup provenance。 | ## Bridge Envelope @@ -165,19 +169,19 @@ type LimeCapabilityInvokeResponse = 稳定错误码首版: -| 错误码 | 含义 | -|---|---| -| `capability_unavailable` | Host 不支持该 capability / method。 | -| `readiness_blocked` | App / entry readiness 未通过。 | -| `permission_denied` | 用户或租户未授权。 | -| `policy_denied` | 企业策略、成本、网络或工具策略阻断。 | -| `schema_invalid` | 请求参数或结果 schema 不匹配。 | -| `source_unverified` | package / manifest / provenance 不可信。 | -| `secret_required` | 缺必需 secret binding。 | -| `timeout` | 调用超时。 | -| `cancelled` | 用户或 Host 取消。 | -| `conflict` | 幂等键、版本或并发状态冲突。 | -| `upstream_failed` | 底层服务失败但边界仍完整。 | +| 错误码 | 含义 | +| ------------------------ | ---------------------------------------- | +| `capability_unavailable` | Host 不支持该 capability / method。 | +| `readiness_blocked` | App / entry readiness 未通过。 | +| `permission_denied` | 用户或租户未授权。 | +| `policy_denied` | 企业策略、成本、网络或工具策略阻断。 | +| `schema_invalid` | 请求参数或结果 schema 不匹配。 | +| `source_unverified` | package / manifest / provenance 不可信。 | +| `secret_required` | 缺必需 secret binding。 | +| `timeout` | 调用超时。 | +| `cancelled` | 用户或 Host 取消。 | +| `conflict` | 幂等键、版本或并发状态冲突。 | +| `upstream_failed` | 底层服务失败但边界仍完整。 | ## 时序图:SDK 初始化 @@ -247,13 +251,13 @@ flowchart TD ## 用户故事 -| 角色 | 用户故事 | 验收 | -|---|---|---| -| 业务用户 | 我在内容工厂页面内启动资料整理、文案生成和复盘,不需要跳回通用 Chat。 | App 内显示任务流、引用、错误、取消、重试和确认写回。 | -| App 开发者 | 我只依赖 `@lime/app-sdk` 类型和 mock host,就能本地测试 App 工作流。 | 不 import Lime internal path;mock / real host 的错误码一致。 | -| 平台维护者 | 我升级 storage、AgentRuntime 或 Tool Broker 时,不需要修改每个 App。 | SDK contract tests 通过;App 只感知兼容版本和稳定错误码。 | -| 企业管理员 | 我可以用 policy / tenant overlay 控制工具、secret、成本和 network。 | Bridge 层强制策略;UI 只展示结果,不承担最终授权。 | -| QA / 智能体 | 我能用 contract tests 和 GUI smoke 证明正式入口与 SDK 边界没有漂移。 | `src/features/agent-app` 无直接 `safeInvoke` / `invoke` / Tauri / raw Worker 越界。 | +| 角色 | 用户故事 | 验收 | +| ----------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| 业务用户 | 我在内容工厂页面内启动资料整理、文案生成和复盘,不需要跳回通用 Chat。 | App 内显示任务流、引用、错误、取消、重试和确认写回。 | +| App 开发者 | 我只依赖 `@lime/app-sdk` 类型和 mock host,就能本地测试 App 工作流。 | 不 import Lime internal path;mock / real host 的错误码一致。 | +| 平台维护者 | 我升级 storage、AgentRuntime 或 Tool Broker 时,不需要修改每个 App。 | SDK contract tests 通过;App 只感知兼容版本和稳定错误码。 | +| 企业管理员 | 我可以用 policy / tenant overlay 控制工具、secret、成本和 network。 | Bridge 层强制策略;UI 只展示结果,不承担最终授权。 | +| QA / 智能体 | 我能用 contract tests 和 GUI smoke 证明正式入口与 SDK 边界没有漂移。 | `src/features/agent-app` 无直接 `safeInvoke` / `invoke` / Tauri / raw Worker 越界。 | ## 用例 @@ -265,16 +269,16 @@ flowchart TD ## 实施拆分 -| 阶段 | 目标 | 主要产物 | 验收 | -|---|---|---|---| -| P18.0 | v0.4 标准差距收口 | 对齐 `agentapp-ref@0.4.0` Host Bridge v1、SDK typed API、错误码 gap matrix。 | 文档 diff check;不改运行代码。 | -| P18.1 | SDK 类型与 schema | 已完成:`sdk` types、capability invoke envelope、stable error enum、mock host。 | `capabilityContract` + `MockCapabilityHost` SDK contract tests;`typecheck`。 | -| P18.2 | Host Bridge router | 已完成:capability invoke typed envelope、`args` / `input` 兼容、requestId / idempotency / expectedSchema / provenance 透传、stable error response。 | bridge router / dispatcher / runtime page tests;未知 capability 不写假成功。 | -| P18.3 | Core capability adapters | 已完成:`lime.ui`、`storage`、`artifacts`、`evidence`、`knowledge`、`tools` typed adapter facade。 | adapter contract tests、namespace / provenance tests、typecheck、contracts。 | -| P18.4 | App-scoped Agent Task | 已完成:`lime.agent.startTask` / stream / get / cancel / retry / submitHostResponse / listTasks typed adapter,并覆盖 `task:*`、`artifact:created`、`evidence:*` 事件契约。 | App task adapter contract tests;不回跳通用 Chat;不重复实现 runtime read model。 | -| P18.4-H | AgentRuntime handoff gate | 已完成:对齐隔壁 current MVP 与剩余缺口,新增消费侧 handoff checklist,并明确 push subscribe、workspace patch producer、capability catalog、GUI smoke owner。 | 只做 SDK 消费侧 gate,不抢 AgentRuntime Rust / TS 修改。 | -| P18.5 | 内容工厂 SDK 化回归 | 已完成 Lime-side SDK regression、Host Bridge SDK client contract 和 package-side 只读 `npm test`;内容工厂 task / write-back / evidence 可由通用 facade 表达,也可穿过 Host Bridge v1。 | 下一刀等待 package owner 稳定后做 P18.5.3 package-side SDK facade / verify;不复刻内容工厂后端能力。 | -| P18.6 | Raw worker 前 gate | 已完成:明确 worker sandbox、resource limit、network / secret policy 进入后续 P19,并记录 P18 不执行 raw worker 的证据。 | P18 不执行 raw worker。 | +| 阶段 | 目标 | 主要产物 | 验收 | +| ------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| P18.0 | v0.4 标准差距收口 | 对齐 `agentapp-ref@0.4.0` Host Bridge v1、SDK typed API、错误码 gap matrix。 | 文档 diff check;不改运行代码。 | +| P18.1 | SDK 类型与 schema | 已完成:`sdk` types、capability invoke envelope、stable error enum、mock host。 | `capabilityContract` + `MockCapabilityHost` SDK contract tests;`typecheck`。 | +| P18.2 | Host Bridge router | 已完成:capability invoke typed envelope、`args` / `input` 兼容、requestId / idempotency / expectedSchema / provenance 透传、stable error response。 | bridge router / dispatcher / runtime page tests;未知 capability 不写假成功。 | +| P18.3 | Core capability adapters | 已完成:`lime.ui`、`storage`、`artifacts`、`evidence`、`knowledge`、`tools` typed adapter facade。 | adapter contract tests、namespace / provenance tests、typecheck、contracts。 | +| P18.4 | App-scoped Agent Task | 已完成:`lime.agent.startTask` / stream / get / cancel / retry / submitHostResponse / listTasks typed adapter,并覆盖 `task:*`、`artifact:created`、`evidence:*` 事件契约。 | App task adapter contract tests;不回跳通用 Chat;不重复实现 runtime read model。 | +| P18.4-H | AgentRuntime handoff gate | 已完成:对齐隔壁 current MVP 与剩余缺口,新增消费侧 handoff checklist,并明确 push subscribe、workspace patch producer、capability catalog、GUI smoke owner。 | 只做 SDK 消费侧 gate,不抢 AgentRuntime Rust / TS 修改。 | +| P18.5 | 内容工厂 SDK 化回归 | 已完成 Lime-side SDK regression、Host Bridge SDK client contract 和 package-side 只读 `npm test`;内容工厂 task / write-back / evidence 可由通用 facade 表达,也可穿过 Host Bridge v1。 | 下一刀等待 package owner 稳定后做 P18.5.3 package-side SDK facade / verify;不复刻内容工厂后端能力。 | +| P18.6 | Raw worker 前 gate | 已完成:明确 worker sandbox、resource limit、network / secret policy 进入后续 P19,并记录 P18 不执行 raw worker 的证据。 | P18 不执行 raw worker。 | ## P18.1 实施记录 @@ -465,23 +469,23 @@ npm run verify:gui-smoke -- --reuse-running --timeout-ms 300000 ## 风险与应对 -| 风险 | 影响 | 应对 | -|---|---|---| -| SDK 过厚,变成 Lime internal API 重导出。 | App 绑定内部实现,升级成本高。 | 只暴露 capability facade;禁止 internal import。 | -| SDK 过薄,App 重复造底座。 | 每个 App 重复做 storage、tool、evidence。 | P18 优先封装高频底座和稳定错误语义。 | -| Bridge 被当成私有协议滥用。 | 每个 App 自定义 postMessage,无法治理。 | App 作者只调用 SDK;Host Bridge 保持标准 v1 信封。 | -| Agent task 退化为模型 API。 | 丢失 trace、tool、knowledge、evidence。 | `lime.agent` 必须走 AgentRuntime Surface。 | -| P18 把 P17.5 证据扩大解释。 | 误以为已可发布 marketplace / Cloud 管理台。 | P17.5 只证明正式入口 smoke;P18 仍按 SDK gate 最小面推进。 | -| 与隔壁任务改同一文件。 | 冲突或覆盖。 | P18 不改 AgentRuntime Rust 投影和运行中 GUI 验证链路。 | +| 风险 | 影响 | 应对 | +| ----------------------------------------- | ------------------------------------------- | ---------------------------------------------------------- | +| SDK 过厚,变成 Lime internal API 重导出。 | App 绑定内部实现,升级成本高。 | 只暴露 capability facade;禁止 internal import。 | +| SDK 过薄,App 重复造底座。 | 每个 App 重复做 storage、tool、evidence。 | P18 优先封装高频底座和稳定错误语义。 | +| Bridge 被当成私有协议滥用。 | 每个 App 自定义 postMessage,无法治理。 | App 作者只调用 SDK;Host Bridge 保持标准 v1 信封。 | +| Agent task 退化为模型 API。 | 丢失 trace、tool、knowledge、evidence。 | `lime.agent` 必须走 AgentRuntime Surface。 | +| P18 把 P17.5 证据扩大解释。 | 误以为已可发布 marketplace / Cloud 管理台。 | P17.5 只证明正式入口 smoke;P18 仍按 SDK gate 最小面推进。 | +| 与隔壁任务改同一文件。 | 冲突或覆盖。 | P18 不改 AgentRuntime Rust 投影和运行中 GUI 验证链路。 | ## 决策点 -| 决策点 | 时间 | 判断问题 | Go 条件 | No-Go 动作 | -|---|---|---|---|---| -| D17 | P17.5 后 | 正式入口 smoke 是否稳定? | 已通过:独立 `smoke:agent-apps` 输出 summary。 | 如后续回归失败,不继续扩大 SDK,先修正式入口。 | -| D18 | P18.2 后 | Host Bridge typed router 是否足够稳定? | 已通过:source / origin / appId / entryKey / requestId / typed envelope / stable error mapping 均有测试。 | 如回归失败,暂停 capability adapter 扩展,先修 router。 | -| D19 | P18.4 后 | App-scoped Agent task 是否真正进入 AgentRuntime Surface? | task / trace / evidence 可追溯,App 内可 stream / cancel / retry / submitHostResponse / write-back,并完成 P18.4-H handoff gate。 | 不开放更多 workflow / worker 能力。 | -| D20 | P18 完成 | SDK 是否足够支撑更多 Agent App? | types / schema / mock / contract / GUI smoke 均有证据,且外部 `content-factory-app` 已从手写 bridge wrapper 收敛到标准 SDK facade,并完成 package verify / Lime regression。 | 已满足;进入 owner handoff / 提交边界,不进入 raw worker 或 marketplace。 | +| 决策点 | 时间 | 判断问题 | Go 条件 | No-Go 动作 | +| ------ | -------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| D17 | P17.5 后 | 正式入口 smoke 是否稳定? | 已通过:独立 `smoke:agent-apps` 输出 summary。 | 如后续回归失败,不继续扩大 SDK,先修正式入口。 | +| D18 | P18.2 后 | Host Bridge typed router 是否足够稳定? | 已通过:source / origin / appId / entryKey / requestId / typed envelope / stable error mapping 均有测试。 | 如回归失败,暂停 capability adapter 扩展,先修 router。 | +| D19 | P18.4 后 | App-scoped Agent task 是否真正进入 AgentRuntime Surface? | task / trace / evidence 可追溯,App 内可 stream / cancel / retry / submitHostResponse / write-back,并完成 P18.4-H handoff gate。 | 不开放更多 workflow / worker 能力。 | +| D20 | P18 完成 | SDK 是否足够支撑更多 Agent App? | types / schema / mock / contract / GUI smoke 均有证据,且外部 `content-factory-app` 已从手写 bridge wrapper 收敛到标准 SDK facade,并完成 package verify / Lime regression。 | 已满足;进入 owner handoff / 提交边界,不进入 raw worker 或 marketplace。 | ## 下一刀口径 diff --git a/docs/roadmap/agentruntime/agent-app-runtime-completion-audit.md b/docs/roadmap/agentruntime/agent-app-runtime-completion-audit.md index 778c8e984..dfb1647b6 100644 --- a/docs/roadmap/agentruntime/agent-app-runtime-completion-audit.md +++ b/docs/roadmap/agentruntime/agent-app-runtime-completion-audit.md @@ -1,7 +1,7 @@ # Agent App Runtime 完成审计 -> 状态:current-harness-export-projection-rust-verified -> 更新时间:2026-05-16 +> 状态:current-harness-export-projection-rust-verified-with-product-shell-gaps +> 更新时间:2026-05-17 > 审计目标:验证“Agent App 在业务 App 内完成 AI Agent 工作,且复用 Lime 完整 AgentRuntime / Claw / Tools / Evidence 能力,不跳回通用 Chat、不退化成模型 API 壳”是否已经真正完成。 最新验收结论:内容工厂真实 Lime 宿主 iframe 已跑通“写文案 -> AgentRuntime hidden session -> artifactDocument JSON patch -> Host Bridge artifact replay -> App 页面物化”的主路径,并补上 App 端产物 contract 验收与一次 continuation。最新实现会按 `minimumCount / minimumScriptCount / minimumImagePromptCount` 校验结构化 patch;不足时先物化可审核草稿,再以 `content_factory.copy.continue` 重新进入 Lime AgentRuntime 补齐,且不新增垂直后端命令。Host Bridge subscription 已在成功终态后短轮询 replay 最终 artifact,`AgentRuntimeCapabilityHost` 也会从 `threadRead.artifacts` 补投 `artifact:created` payload;订阅还会监听 `agent_app_runtime:{appId}:{taskId}` Tauri / DevBridge runtime event,把后端 runtime event 直接转发给 App。AgentRuntime profile 事件现在会在同名 `agent_app_runtime:{appId}:{taskId}` event bus 上主动追加 `agent_app_runtime:profileProjection` payload,并携带 canonical `taskEvents`;高价值 `RuntimeAgentEvent` 也会主动追加 `agent_app_runtime:runtimeEventProjection`,其中 `ArtifactSnapshot / FileArtifact` 可直接携带 `workspacePatch / contentFactoryWorkspacePatch`;显式写在 runtime event / timeline metadata 中的 `evidenceRefs / verificationOutcomes` 也会通过 `runtime_evidence_projection_service` 投影为 `evidence:recorded / evidence:verified`。本轮进一步把 `agent_runtime_export_evidence_pack`、`agent_runtime_export_analysis_handoff`、`agent_runtime_export_review_decision_template` 与 `agent_runtime_save_review_decision` 接入同一个 `agent_app_runtime:{appId}:{taskId}` event bus:当 hidden session 的 `runtime_summary.surface=agent_app` 且带 `appId/taskId` 时,导出结果会主动 emit `agent_app_runtime:harnessExportProjection`,把 Evidence Pack root、analysis/review 制品和 completion audit completed 事实投影为 App canonical `taskEvents`。因此 App 不再只能等 `getTask` 副作用获得运行进度、artifact patch、已存在的 evidence refs 或 Harness 导出结果。Rust facade 现在会把多个 capability hint 去重后写入 `agent_app_runtime.capability_workflow` 与 `harness.agent_app_runtime_capability_workflow`;内容工厂复合 output contract 使用 `metadata_only`,不会被强行改写成单一 Claw Skill。Host dispatcher 现在会校验 manifest `toolRefs[].capabilities` allowlist,防止 App 只声明 catalog key 后任意请求 Claw capability。Playwright 中已验证真实 iframe 可通过 replay 回写并显示 `文案=20 / 脚本=6 / 图片需求=5`,控制台 error 为 0。 @@ -37,6 +37,9 @@ | 跨刷新恢复 task/session | done/first-cut | `src/features/agent-app/runtime/agentRuntimeCapabilityHost.ts`、`src/features/agent-app/runtime/agentRuntimeCapabilityHost.test.ts` | runtime task state 写入 Agent App storage;Host 重建后可恢复 `sessionId / turnId` 并继续读取/响应。 | | 内容工厂实际 App 接入 Host Bridge | done/first-cut | `/Users/coso/Documents/dev/ai/limecloud/content-factory-app/src/ui/host-bridge.js`、`src/ui/app.js`、`tests/ui.test.mjs` | 场景、文案、重写、脚本、确认链会先启动 Lime Agent 任务,并保留本地兜底。 | | 内容工厂 App 内显示任务进度,不跳回 Chat | done/first-cut | `/Users/coso/Documents/dev/ai/limecloud/content-factory-app/src/ui/app.js`、`src/ui/host-bridge.js`、Playwright MCP 真实宿主验证 | 新增 `lime:host-agent-task-update`,主生产流通过 Host task subscription 更新“AI 同事任务”面板;页面显示“等待 AgentRuntime 调度或回写进度”,不再把 runtime `idle` 暴露为任务终态。 | +| Agent App 打开时让出业务画布,且用户可手动恢复导航 | done/gui-verified | `src/components/AppSidebar.tsx`、`src/components/AppSidebar.test.tsx`、`npm test -- "src/components/AppSidebar.test.tsx"`、`npm run typecheck`、Playwright MCP `http://127.0.0.1:1420/` | 进入 `agent-app` 时 Lime 主侧栏默认临时折叠到 72px;用户点击“展开导航栏”后本次停留期间保持 272px 展开;离开 Agent App 后仍为进入前的 272px,且临时折叠不写入用户偏好。Playwright 复核中控制台仅有 1 条进入前已有的 `AsterChat` bridge timeout error,未新增侧栏相关错误。 | +| `AI 同事` 是所有 Agent App 的通用 Host 模式,而非内容工厂私有右栏 | partial/product-gap | 内容工厂已有 `AI 同事` 样板;`src/features/agent-app/runtime/hostBridge.test.ts` 已出现 canonical `runtimeProcess` 优先投影;`src/features/agent-app/runtime/agentRuntimeProcess.ts` first-cut 可归一化 timeline / thinking / execution / Skills / model / usage / cost | 产品边界已明确:Host 提供通用 Agent coworker shell / runtimeProcess slot / capability facts,业务 App 只提供页面上下文、任务合同和结果物化。当前还没有把内容工厂私有右栏抽成 Agent App Host 级共享 shell,因此不能宣称所有模块已统一。 | +| 运行过程完成后折叠但不消失,且包含思考、执行、Skill、工具、模型、Token、费用 | partial/code-first-cut | `src/features/agent-app/runtime/agentRuntimeProcess.ts`、`src/features/agent-app/runtime/agentRuntimeProcess.test.ts`、`npm test -- "src/features/agent-app/runtime/agentRuntimeProcess.test.ts"` | first-cut 已能保留完整 timeline,不在数据层截断到最后 16 条,并保留每段流式输出;代码也有 thinking / execution / skillNames / invokedSkillNames / model / usage / cost projection。该文件仍处于并行未跟踪写集,且缺真实 Host 业务流 GUI 复核,所以只算 first-cut evidence,不能算完成。 | | 内容工厂可由 runtime structured patch 驱动 workspace | done/current-artifact-replay-verified | `/Users/coso/Documents/dev/ai/limecloud/content-factory-app/src/ui/app.js`、`tests/ui.test.mjs`、Playwright 最新真实宿主流 | App 会从 task / snapshot / event payload 以及 artifactDocument fenced JSON 中提取 `workspacePatch` / `contentFactoryWorkspacePatch`,并把数组形态的 `sceneTable / contentBatch / scripts / imagePrompts` 归一化成当前 workspace UI 可消费结构;真实 Host Bridge replay 已把最终 artifact 物化为 `文案=20 / 脚本=6 / 图片需求=5`。 | | 内容工厂向 runtime 提供业务上下文,而不是只传 model prompt | done/current-verified | `/Users/coso/Documents/dev/ai/limecloud/content-factory-app/src/ui/app.js` | `runProduction / runScenarios / onlyCopy / runScripts` 均传入 `projectContext`,包含项目、已确认知识条目、readiness、确认链和已有场景摘要,避免 AgentRuntime 因只看到 `materialVersion` 文件名而停在索要资料。 | | 内容工厂结果写回 storage/artifact/evidence | done/first-cut | `/Users/coso/Documents/dev/ai/limecloud/content-factory-app/src/ui/host-bridge.js`、`src/ui/app.js`、`APP.md` | 只使用 manifest 声明的 `scene_table / content_batch / script_batch` 与 `fact_grounding / publish_readiness`。 | @@ -60,6 +63,12 @@ | --- | --- | --- | | `npm run typecheck` | passed | Lime 前端类型边界;覆盖 `agentAppRuntime` 新增 `providerPreference / modelPreference` 类型透传。 | | `npm run typecheck` | passed/current-event-listen | 覆盖 `toolRefs[].capabilities` 类型投影和 Host Bridge runtime event listener 类型边界。 | +| `npm test -- "src/components/AppSidebar.test.tsx"` | passed/current-product-shell | 57 tests passed;覆盖 Agent App 页面进入时主侧栏临时折叠、手动展开后不被自动压回、离开后恢复进入前偏好。 | +| `npm run typecheck` | passed/current-product-shell | 覆盖 AppSidebar 临时折叠状态机和当前工作树 TypeScript 类型边界。 | +| Playwright MCP:Lime 首页 -> 内容工厂 -> 展开导航 -> 返回首页 | passed/current-product-shell | `http://127.0.0.1:1420/`;点击“内容工厂”后侧栏 72px,点击“展开导航栏”后侧栏 272px,等待 1s 仍保持展开,点击“返回 Lime 首页”后保持进入前 272px。控制台 error 总数 1,来自进入前已有 `aster_agent_init` bridge timeout,不是本轮侧栏回归。 | +| Playwright MCP:内容工厂 `AI 同事` 抽屉 | passed/content-factory-sample | 在 Lime Host iframe 内点击“打开 AI 同事”后出现 `AI 同事` dialog,文案说明“运行过程、确认项和证据链都在这里,不挤压当前工作页面”,并展示运行现场、系统建议、确认链、待确认、证据链;点击“关闭 AI 同事”可关闭。该验证只证明内容工厂样板可用,不证明 Host 级通用 shell 已完成。 | +| `npm test -- "src/features/agent-app/runtime/agentRuntimeProcess.test.ts"` | passed/current-process-first-cut | 2 tests passed;覆盖运行过程 timeline 不截断,以及流式输出片段不被数据层折叠丢失。该测试来自并行未跟踪写集,只能证明当前工作树 first-cut 行为。 | +| `npm test -- "src/features/agent-app/runtime/hostBridge.test.ts" "src/features/agent-app/runtime/agentRuntimeProcess.test.ts"` | passed/current-process-first-cut | 16 tests passed;覆盖 Host Bridge canonical `runtimeProcess` 投影、task subscription,以及 runtimeProcess first-cut。该验证仍是当前工作树验证,不代表真实 Host 业务流已经复核。 | | `npm run test:contracts` | passed | Lime Tauri command 四侧、runtime client、harness 与 modality contract。 | | `npm run test:contracts` | passed/current | 本轮新增模型偏好字段和 App task 类型后,命令四侧、runtime client、harness 与 modality contract 仍通过。 | | `npm run test:contracts` | passed/current-artifact-replay | Host Bridge / AgentRuntime artifact replay 没有新增垂直 `content_factory_*` 命令,也没有扩展 deprecated command surface;四侧契约仍通过。 | @@ -126,6 +135,6 @@ ## 5. 完成判定 -按“Agent App 能在 App 内调用完整 Lime AgentRuntime,并完成内容工厂主流程第一刀”的口径,当前已经达到 **current MVP**。真实 Lime 宿主 iframe Host Bridge 已有端到端证据:内容工厂在 Lime 页面内可发现 bridge、发起任务、通过 task subscription 显示进度,使用隐藏 `agent-app-runtime-*` session,不跳回通用 Chat,并把 AgentRuntime 产出的 `contentFactoryWorkspacePatch` 物化成 App 页面内的文案、脚本和图片需求。 +按“Agent App 能在 App 内调用完整 Lime AgentRuntime,并完成内容工厂主流程第一刀”的基础 runtime 口径,当前已经达到 **current MVP**。真实 Lime 宿主 iframe Host Bridge 已有端到端证据:内容工厂在 Lime 页面内可发现 bridge、发起任务、通过 task subscription 显示进度,使用隐藏 `agent-app-runtime-*` session,不跳回通用 Chat,并把 AgentRuntime 产出的 `contentFactoryWorkspacePatch` 物化成 App 页面内的文案、脚本和图片需求。 -按用户要求的“完整实现整体目标”口径,当前仍不能标记 100% 完成:内容工厂 App 端产物 contract 强校验、continuation、Host Bridge terminal artifact replay、真实 iframe 物化、seeded manifest 写回类型对齐、high-level Host capability gate、Claw capability allowlist gate、多 capability workflow metadata、runtime event listen、`getTask` snapshot projection、AgentRuntime profile event 主动 App task projection,以及高价值 RuntimeAgentEvent 主动 App task projection 已完成 first-cut。剩余缺口已从“是否能主动推送 App 任务事件 / artifact patch / 显式 evidence refs / Harness 导出结果”缩小为:capability catalog 仍缺真正多能力执行编排、后端/cross-surface policy owner 和跨 surface 统一 owner;新 Harness export projection 也还没有经过重启后的 Playwright 真实生成流覆盖。下一刀应优先补 **capability policy owner + 真正多能力执行编排**,并在安全窗口重启宿主后用 Playwright 跑一次包含 Evidence Pack / analysis / review projection 的真实业务流。当前整体目标完成度约 **99.92%**。 +按用户最新要求的“完整实现整体目标”口径,当前仍不能标记 100% 完成,也不能只按 runtime MVP 的通过情况宣称完成:内容工厂 App 端产物 contract 强校验、continuation、Host Bridge terminal artifact replay、真实 iframe 物化、seeded manifest 写回类型对齐、high-level Host capability gate、Claw capability allowlist gate、多 capability workflow metadata、runtime event listen、`getTask` snapshot projection、AgentRuntime profile event 主动 App task projection,以及高价值 RuntimeAgentEvent 主动 App task projection 已完成 first-cut;主侧栏进入 Agent App 自动让出空间且可手动展开已完成代码验证。剩余缺口是产品化主链缺口:`AI 同事` 还没有沉淀成所有 Agent App 复用的 Host 级 shell;`runtimeProcess` 的不丢过程、思考、执行、Skill、模型、Token、费用展示仍缺真实 Host 流 GUI 复核;内容工厂知识库、项目驾驶舱、交付、复盘等页面仍需全部按 Agent Run 重设计或下线伪 AI 动作;capability catalog 仍缺真正多能力执行编排、后端/cross-surface policy owner 和跨 surface 统一 owner;新 Harness export projection 也还没有经过重启后的 Playwright 真实生成流覆盖。下一刀应优先补 **Host 通用 AI 同事 shell + runtimeProcess 合入验证**,随后再补 **capability policy owner + 真正多能力执行编排**,并在安全窗口重启宿主后用 Playwright 跑一次包含 Evidence Pack / analysis / review projection 的真实业务流。当前 Agent App runtime MVP 完成度约 **90%**;按“内容工厂所有 AI 页面都在 App 内完整展示 Lime Agent 能力”的产品目标,整体完成度约 **65%**。 diff --git a/docs/roadmap/agentruntime/app-surface-runtime.md b/docs/roadmap/agentruntime/app-surface-runtime.md index 1bd91d775..30d6ec281 100644 --- a/docs/roadmap/agentruntime/app-surface-runtime.md +++ b/docs/roadmap/agentruntime/app-surface-runtime.md @@ -37,22 +37,59 @@ Content Factory / Agent App ## 3. Surface 边界 -| 层 | current 责任 | 禁止事项 | -| --- | --- | --- | -| Agent App UI | 业务形态、表单、资料版本、场景、文案、素材、交付和复盘 | 直接访问 Tauri / Node / 宿主 DOM,绕过 Host Bridge | -| Agent App Workflow | 从 App 状态组装 task input、expected output、review/write-back 目标 | 自己维护第二套 Agent task runtime | -| Host Bridge | iframe sandbox、主题、语言、导航、capability transport、安全校验 | 承载业务 Agent 逻辑 | -| Agent App Runtime Surface | 把 App task 映射到 AgentRuntime control plane,附加 app provenance | 复制 Claw skill launch 或绕过 `agent_runtime_*` 主链 | -| AgentRuntime | session/thread/turn/task、queue、event、tool、policy、evidence facts | 决定垂直业务 UI 长什么样 | -| Claw Capability Catalog | 把 `@配图`、`@搜索`、`@研报` 等能力变成可复用 capability | 继续把能力绑定死在 Chat/Inputbar 字符串分支 | -| Artifact / Evidence | 产物、引用、工具调用、知识版本、人工确认和验证记录 | 让最终结果只停留在聊天文本 | +| 层 | current 责任 | 禁止事项 | +| ------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------- | +| Agent App UI | 业务形态、表单、资料版本、场景、文案、素材、交付和复盘 | 直接访问 Tauri / Node / 宿主 DOM,绕过 Host Bridge | +| Agent App Workflow | 从 App 状态组装 task input、expected output、review/write-back 目标 | 自己维护第二套 Agent task runtime | +| Host Bridge | iframe sandbox、主题、语言、导航、capability transport、安全校验 | 承载业务 Agent 逻辑 | +| Agent App Runtime Surface | 把 App task 映射到 AgentRuntime control plane,附加 app provenance | 复制 Claw skill launch 或绕过 `agent_runtime_*` 主链 | +| AgentRuntime | session/thread/turn/task、queue、event、tool、policy、evidence facts | 决定垂直业务 UI 长什么样 | +| Claw Capability Catalog | 把 `@配图`、`@搜索`、`@研报` 等能力变成可复用 capability | 继续把能力绑定死在 Chat/Inputbar 字符串分支 | +| Artifact / Evidence | 产物、引用、工具调用、知识版本、人工确认和验证记录 | 让最终结果只停留在聊天文本 | + +### 运行过程封装边界 + +`thinking / text delta / tool input-output / Skill / routing / cost / usage / artifact / evidence / blocked / completed` 这类 Claw 式运行过程属于 Lime 主 App 的 runtime projection,不属于垂直业务 App。Agent App 只允许消费 Host Bridge 下发的 `runtimeProcess` / `process` 标准视图,并用它做业务展示: + +- `runtimeProcess.timeline`:可折叠的运行现场,运行中展开,终态默认折叠但不丢失。 +- `runtimeProcess.streamText / thinkingText / executionText`:正文、思考和执行片段。 +- `runtimeProcess.model / usage / cost`:模型路由、Token 和费用事实。 +- `runtimeProcess.skillNames / invokedSkillNames`:本轮 required Skills 与真实调用情况。 +- `runtimeProcess.terminal / collapsedByDefault`:由主 App 判断运行生命周期,App 不再自己推断底层状态。 + +因此内容工厂、未来销售助手或交付助手都不应该复制一份 `agent-runtime-process` 解析器;它们只处理业务表单、业务产物、人工确认和工作流递进。若某个 App 发现 process 信息缺失,应该回报 Host / Runtime projection 缺口,而不是在 App 内补第二套底层归一化。 事实源声明: ```text Agent App 的完整 AI 能力只允许向 AgentRuntime Surface + AgentRuntime facts 收敛。 +Agent App 的运行过程 UI 事实只允许向 AgentRuntimeCapabilityHost + HostBridge runtimeProcess 收敛。 ``` +## 3.1 全量 `lime.*` 能力与 AgentRuntime 的关系 + +Agent App 侧全量能力路线图见 [`../agentapp/p18-7-full-lime-capability-surface.md`](../agentapp/p18-7-full-lime-capability-surface.md)。本文只固定 AgentRuntime owner 边界:凡涉及 AI 执行、模型、Skill、工具、记忆、用量、证据的能力,生产事实必须继续向 AgentRuntime / ToolRuntime / Desktop Host 收敛。 + +Agent App 不再只抽象 `lime.agent / lime.workflow` 两个入口。完整 Lime AI 能力必须拆成三类,并统一登记在 `src/features/agent-app/sdk/capabilityCatalog.ts`: + +| 类型 | Capability | AgentRuntime 关系 | App 边界 | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| 执行主链 | `lime.agent`、`lime.workflow`、`lime.automation`、`lime.tasks` | 进入 AgentRuntime session/thread/turn/task/queue。 | App 只定义业务输入、状态递进和结果写回。 | +| 智能体资源 | `lime.models`、`lime.skills`、`lime.memory`、`lime.context`、`lime.usage` | 从 AgentRuntime request metadata、runtime facts、read model、telemetry 投影。 | App 不保存 provider key,不复制 Skill runtime,不自建 token/cost 统计。 | +| 工具与集成 | `lime.tools`、`lime.mcp`、`lime.browser`、`lime.search`、`lime.documents`、`lime.media`、`lime.terminal`、`lime.connectors` | 经 ToolRuntime / MCP bridge / Browser / Media / Document runtime 接入 AgentRuntime evidence。 | App 声明需求和消费结构化结果,不直连底层服务。 | +| 数据与证据 | `lime.storage`、`lime.files`、`lime.knowledge`、`lime.artifacts`、`lime.evidence` | 与 AgentRuntime task provenance、artifact refs、knowledge refs 关联。 | App 定义业务对象和产物类型,Lime 负责命名空间、引用和审计。 | +| 治理与宿主 | `lime.ui`、`lime.events`、`lime.workspace`、`lime.policy`、`lime.secrets`、`lime.settings`、`lime.review`、`lime.capabilities` | 作为 Host / policy / readiness projection 约束 runtime 调用。 | App 做展示和降级,不绕过 Host Bridge。 | + +事实源声明补强: + +```text +所有可给 Agent App 使用的 Lime 功能,名称和边界先进入 lime.* capability catalog。 +所有涉及 AI 执行、模型、Skill、工具、记忆、用量、证据的能力,生产事实必须继续向 AgentRuntime / ToolRuntime / Desktop Host 收敛。 +业务 App 只消费 SDK facade 与 runtimeProcess projection,不拥有底层执行事实源。 +``` + +这解决内容工厂当前暴露出来的问题:如果 App 需要完整 Lime AI 能力,不是把 Claw UI 嵌进去,也不是让 App 调模型 API,而是把 Claw 已有的图片、搜索、研报、PDF、总结、PPT、浏览器、发布等能力拆成 `lime.skills / lime.tools / lime.search / lime.media / lime.documents / lime.browser / lime.artifacts / lime.evidence` 等能力,再由 `lime.agent.startTask` 或 `lime.workflow.start` 编排。缺失的能力只允许在 catalog/profile 中显示为 unavailable,不允许由业务 App 临时 mock 成成功。 + ## 4. current / compat / deprecated / dead ### current @@ -122,17 +159,17 @@ humanReview = true 最小事件: -| 事件 | App 内展示 | -| --- | --- | -| `task:started` | 本轮任务已启动 | -| `task:contextChecked` | 资料、场景、脚本和知识绑定检查结果 | -| `task:missingContextRequested` | 在当前表单或侧栏追问缺口 | -| `task:toolCall` | 搜索、知识检索、图片、报告、PDF 等工具进度 | -| `task:citation` | 资料、场景、网页、文件引用 | -| `task:partialArtifact` | 草稿文案、素材 brief、脚本片段 | -| `task:reviewRequested` | 用户确认、编辑、拒绝或重试 | -| `task:completed` | 写回完成,可导出交付物 | -| `artifact:created` / `evidence:recorded` | 交付物和证据已落库 | +| 事件 | App 内展示 | +| ---------------------------------------- | ------------------------------------------ | +| `task:started` | 本轮任务已启动 | +| `task:contextChecked` | 资料、场景、脚本和知识绑定检查结果 | +| `task:missingContextRequested` | 在当前表单或侧栏追问缺口 | +| `task:toolCall` | 搜索、知识检索、图片、报告、PDF 等工具进度 | +| `task:citation` | 资料、场景、网页、文件引用 | +| `task:partialArtifact` | 草稿文案、素材 brief、脚本片段 | +| `task:reviewRequested` | 用户确认、编辑、拒绝或重试 | +| `task:completed` | 写回完成,可导出交付物 | +| `artifact:created` / `evidence:recorded` | 交付物和证据已落库 | 当前已落地第一刀:`agent_app_runtime_get_task` 会从 `AgentRuntimeThreadReadModel` 投影 `taskStatus` 与 `taskEvents`,先覆盖 queued / progress / missing context / review request / tool call / artifact created / evidence recorded / evidence verified / completed / cancelled / error / incident。AgentRuntime profile event 生成处也会主动 emit `agent_app_runtime:profileProjection`,把 `turn.* / tool.* / action.* / routing.* / model.*` 投影成 App canonical `taskEvents`;高价值 `RuntimeAgentEvent` 会主动 emit `agent_app_runtime:runtimeEventProjection`,其中 `ArtifactSnapshot / FileArtifact` 可直接携带 `workspacePatch / contentFactoryWorkspacePatch`,显式写入 runtime event / timeline metadata 的 `evidenceRefs / verificationOutcomes` 也会通过 `runtime_evidence_projection_service` 投影为 `evidence:recorded / evidence:verified`。`AgentAppRuntimePage` 已消费这组 snapshot / profile / runtime projection 事件并转成 App 可见的 `AgentAppTaskStreamEvent`;App 也可以通过 `lime.agent.submitHostResponse` 把 ask_user / elicitation / tool_confirmation 响应回 `agent_app_runtime_submit_host_response`。`AgentRuntimeCapabilityHost` 已把 `taskId / sessionId / turnId / request / provenance` 持久化到 Agent App storage,刷新或重建 Host 后可以继续 `getTask / listTasks / submitHostResponse`,并能从 `threadRead.artifacts` 补投 `artifact:created` payload。Host Bridge 已支持 `capability:subscribe / capability:unsubscribe / capability:event`:订阅时会通过 `safeListen` 监听 `agent_app_runtime:{appId}:{taskId}` Tauri / DevBridge runtime event,并把后端 runtime event 和 projection 推给 iframe;同时保留 Host 侧 `getTask` 轮询作为 snapshot / artifact replay fallback。成功终态如果暂未带 workspace patch,会继续短轮询最多 4 次等待最终 artifact replay,已经能让 App 留在当前页面持续收到 task update、runtime event 和最终 artifact patch。Harness export projection 也已接入同一 event bus:Evidence Pack / analysis / review / save review 导出成功后,会把导出 root、制品列表和 completion audit completed 事实投影为 App task events。 diff --git a/docs/roadmap/agentruntime/host-agent-run-ui-sdk.md b/docs/roadmap/agentruntime/host-agent-run-ui-sdk.md new file mode 100644 index 000000000..0bbdefa4a --- /dev/null +++ b/docs/roadmap/agentruntime/host-agent-run-ui-sdk.md @@ -0,0 +1,157 @@ +# Host Agent Run UI SDK + +> 状态:current-minimum-slice +> 更新时间:2026-05-17 +> 目标:把 Claw 类 AI 运行现场上移为 Lime Host 通用 UI 能力,让内容工厂和后续 Agent App 不再重复实现思考、执行、Skill、工具、模型、Token、费用、证据链和确认链展示。 + +## 1. 为什么需要这一层 + +Agent App 的价值不是把 Lime Chat 换成 iframe,也不是让业务 App 直接调模型 API。Agent App 应该把业务流程做成合适的产品形态,同时继续复用 Lime 已有 AgentRuntime / Claw / ToolRuntime / Skill / Evidence 能力。 + +内容工厂暴露出的核心问题是:业务 App 一旦需要真实 AI Agent,就会被迫重新实现一套“AI 同事”侧栏,包括流式输出、思考过程、工具调用、Skill 调用、模型选择、Token、费用、Evidence 和人工确认。这会带来三类错误: + +- 每个 App 复制一份运行 UI,体验和数据口径漂移。 +- App 误以为自己需要直连模型 API,退化成普通 Web App。 +- Claw 已有能力无法共享,Lime 的 AgentRuntime 事实源被拆散。 + +因此需要 Host 级 `lime.ui.openAgentRun / updateAgentRun / closeAgentRun`,由 Lime 主 App 提供统一运行面板,业务 App 只声明“我要展示哪次 Agent Run”。 + +## 2. 责任边界 + +| 层 | 应该做什么 | 不应该做什么 | +| --- | --- | --- | +| Lime Host UI | 提供通用 Agent Run 面板、抽屉/弹窗/子页容器、运行过程折叠、模型/Token/费用/Skill/Evidence/确认链展示 | 写死内容工厂业务字段,或让每个 App 自己复制 Claw 渲染 | +| AgentRuntime / ToolRuntime | 产出 `runtimeProcess`、timeline、stream、thinking、execution、tool/skill、artifact、evidence、usage、cost 事实 | 决定业务页面布局,或把结果只塞进聊天文本 | +| SDK / Host Bridge | 暴露 `lime.ui.openAgentRun/updateAgentRun/closeAgentRun`,把 App 请求路由到 Host UI | 让 App 绕过 Host DOM 或私有 postMessage 协议 | +| 业务 Agent App | 定义业务流程、任务 contract、expected output、artifact adapter、人工确认和写回 | 自建底层 Agent Runtime、直连 provider key、复制 Skill runtime、长期维护私有 AI 过程 UI | +| 内容工厂 | 规划项目资料、场景、文案、脚本、图片需求、交付和复盘 | 把所有 AI 思考/工具/Skill/用量渲染逻辑长期留在 App 内 | + +边界结论: + +```text +Agent App 可以有很多个,但 Agent Run UI 只能有一个 Host 事实源。 +业务 App 可以决定何时打开、如何命名、如何把产物落回业务状态;不能拥有模型、Skill、Tool、Token、费用和 Evidence 的底层事实源。 +``` + +## 3. SDK 合同 + +### 3.1 `lime.ui.openAgentRun` + +App 在启动或恢复 Agent task 后调用: + +```ts +await lime.ui.openAgentRun({ + taskId, + bridgeAction: "content_factory.production", + title: "生成内容批次", + mode: "drawer", + expectedOutput: { artifactKind: "content_batch" }, +}); +``` + +Host 返回: + +```ts +{ + opened: true, + surface: "host_agent_run", + mode: "drawer", + taskId +} +``` + +### 3.2 `lime.ui.updateAgentRun` + +App 或 Host Bridge 在拿到 task snapshot、subscription event、`runtimeProcess`、runtime facts 后调用,用于刷新同一个 Host 面板: + +```ts +await lime.ui.updateAgentRun({ + taskId, + title, + runtimeProcess, + events, + task, + snapshot, +}); +``` + +Host UI 必须保证:运行中可展开过程;终态默认折叠但过程不消失。 + +### 3.3 `lime.ui.closeAgentRun` + +App 可以在用户关闭业务页面、取消任务、或希望释放视图时调用。关闭 UI 不等于取消 Agent task;取消仍走 `lime.agent.cancelTask`。 + +## 4. 渲染原则 + +Host 统一 Agent Run UI 第一刀只做通用容器,不急着复刻完整 Claw: + +- 展示标题、taskId、bridgeAction 和来源 App。 +- 展示模型、Token、费用、Skill 约束和真实调用情况。 +- 读取 `runtimeProcess.timeline`,运行中展开,完成后折叠但不删除。 +- 读取 `events / taskEvents / runtimeFacts`,把确认链、交付物和 Evidence 单独收成事实栏,避免业务 App 只在自家面板里展示这些关键过程。 +- 按 `timeline.kind` 呈现 routing / skill / tool / execution / artifact / warning / completed 的轻量语义标记,让 Host 面板接近 Claw 的运行现场,而不是普通日志列表。 +- 展示 `thinkingText / executionText / streamText` 的入口,但不让业务 App 自己解析底层事件。 +- 后续可把 Claw 已有 renderer 下沉成共享 `AgentRunRenderer`,由 Host UI 调用,而不是 iframe App import Claw React 组件。 + +当前代码边界: + +- `src/features/agent-app/ui/AgentRunHostDrawer.tsx` 是 Host 级 Agent Run UI 的第一份可复用组件。 +- `AgentRunProcessPanel` 是抽屉、弹窗或未来 Claw 共享 renderer 都可复用的运行过程面板;Host Shell 负责容器,Process Panel 负责运行事实渲染。 +- `src/features/agent-app/ui/AgentAppRuntimePage.tsx` 只负责 iframe runtime、Host Bridge 生命周期和 `lime.ui.*AgentRun` state 连接。 +- 后续如果 Claw renderer 抽包,应该替换 `AgentRunHostDrawer` 内部 timeline/text 渲染器,而不是再把渲染逻辑塞回 `AgentAppRuntimePage` 或业务 App。 + +## 5. 内容工厂过渡策略 + +当前内容工厂已有 App 内 `AI 同事` 面板,这是过渡实现,不是目标架构。退出条件: + +1. 内容工厂启动 Agent task 后调用 `lime.ui.openAgentRun`。 +2. task subscription / stream / snapshot 更新时调用 `lime.ui.updateAgentRun`。 +3. App 内面板只作为 Host UI 不可用时的 fallback。 +4. 所有新 App 默认接 Host UI SDK,不再新写私有 `host-task-process` 解析器。 +5. 真实 Claw 过程渲染、模型选择、Token、费用、Evidence、确认链继续在 Lime Host / AgentRuntime 层收敛。 + +## 6. 本轮最小交付 + +本轮先实现最小垂直切片: + +- SDK contract / catalog 增加 `openAgentRun / updateAgentRun / closeAgentRun`。 +- Host Bridge 拦截 `lime.ui` capability invoke,支持 toast / navigation / download / snapshot 和 Agent Run UI 请求。 +- `AgentAppRuntimePage` 渲染 Host 级统一 Agent Run 抽屉。 +- 内容工厂调用 Host UI SDK,保留 App 内面板作为 fallback。 + +不在本轮完成: + +- 不把 Claw 的完整 React renderer 抽包。 +- 不新增 `content_factory_*` Tauri 命令。 +- 不动 0.7 manifest/reference CLI 并行写集。 +- 不把 App 内所有页面一次性重设计完成。 + +## 7. 验收口径 + +最小验收: + +- App 通过 SDK 调用 `lime.ui.openAgentRun` 后,Host 页面出现通用 AI 运行抽屉。 +- 抽屉能显示 taskId、标题、模型、Token、费用、Skill 和 timeline。 +- `updateAgentRun` 后 Host 抽屉内容刷新。 +- `closeAgentRun` 只关闭 UI,不取消 Agent task。 +- 内容工厂在 Host 不支持该能力时仍能用本地 fallback,不中断业务流。 + +完整验收: + +- Host UI 使用 Claw 共享 renderer,而不是重新复制样式。 +- Evidence Pack、analysis handoff、review decision、人工确认都能在同一个 Host Run UI 中追踪。 +- 内容工厂全流程页面不再长期维护私有底层 Agent 过程解析。 + +## 8. 当前完成审计 + +| 要求 | 现有证据 | 结论 | +| --- | --- | --- | +| App 能通过 SDK 打开 Host Run UI | `AgentAppRuntimePage.test.tsx` 覆盖 `lime.ui.openAgentRun / updateAgentRun / closeAgentRun` | done/first-cut | +| 完成后折叠但过程不消失 | Host Run dock / drawer 保留同一份 `agentRunUi`,测试断言终态仍能看到历史 timeline | done/first-cut | +| 思考、执行、成稿流式输出不被 App 私有解析 | `AgentRunHostDrawer.tsx` 直接读取 `runtimeProcess.thinkingText / executionText / streamText` | done/first-cut | +| Tool / Skill / 模型路由 / 产物等过程可区分 | `AgentRunHostDrawer.tsx` 读取 `timeline.kind` 并输出 `data-agent-run-timeline-kind` 语义标记;测试覆盖 `routing / skill / tool / execution / completed` | done/first-cut | +| Host UI 不只绑定抽屉一种形态 | `AgentRunProcessPanel` 从 `AgentRunHostDrawer` 抽出,后续可被 modal/page/Claw shared renderer 复用;测试断言 `agent-run-process-panel` 存在 | done/first-cut | +| 模型、Token、费用、Skill 由 Host 统一展示 | `AgentRunMetricCards` 读取 `runtimeProcess.model / usage / cost / skillNames / invokedSkillNames`;测试覆盖模型、Token、费用、Skill | done/first-cut | +| 证据链、交付物、确认链不丢 | `AgentRunFactRail` 读取 `events / taskEvents / runtimeFacts`;测试覆盖 review request、artifact、evidence | done/first-cut | +| 真正复用 Claw renderer | 目前仍是 Host Run 专用 renderer,未从 Claw 抽出共享 React renderer | missing | +| 内容工厂移除私有 AI 面板 | 内容工厂已接 Host UI SDK,但 App 内 AI 面板仍作为 fallback | partial | diff --git a/scripts/agent-apps-smoke.mjs b/scripts/agent-apps-smoke.mjs index d7be17adb..93ea8c787 100644 --- a/scripts/agent-apps-smoke.mjs +++ b/scripts/agent-apps-smoke.mjs @@ -1,11 +1,15 @@ #!/usr/bin/env node import fs from "node:fs"; +import { execFile } from "node:child_process"; import os from "node:os"; import path from "node:path"; import process from "node:process"; +import { promisify } from "node:util"; import { chromium } from "playwright"; +const execFileAsync = promisify(execFile); + const DEFAULTS = { appUrl: "http://127.0.0.1:1420/", healthUrl: "http://127.0.0.1:3030/health", @@ -13,6 +17,9 @@ const DEFAULTS = { intervalMs: 1_000, evidenceDir: path.join(process.cwd(), ".lime", "qc", "gui-evidence", "agent-apps"), prefix: "agent-apps-smoke", + includeContentFactoryActionE2e: false, + includeContentFactoryCompletionE2e: false, + completionTimeoutMs: 90_000, }; const ACCOUNT_MENU_BUTTON_SELECTOR = '[data-testid="app-sidebar-account-button"]'; @@ -53,6 +60,18 @@ function parseArgs(argv) { if (arg === "--prefix" && argv[index + 1]) { options.prefix = String(argv[index + 1]).trim(); index += 1; + continue; + } + if (arg === "--include-content-factory-action-e2e") { + options.includeContentFactoryActionE2e = true; + } + if (arg === "--include-content-factory-completion-e2e") { + options.includeContentFactoryActionE2e = true; + options.includeContentFactoryCompletionE2e = true; + } + if (arg === "--completion-timeout-ms" && argv[index + 1]) { + options.completionTimeoutMs = Number(argv[index + 1]); + index += 1; } } return options; @@ -72,6 +91,836 @@ function logStage(stage) { console.log(`[smoke:agent-apps] stage=${stage}`); } +function resolveInvokeUrl(healthUrl) { + try { + const url = new URL(healthUrl); + url.pathname = "/invoke"; + url.search = ""; + url.hash = ""; + return url.toString(); + } catch { + return "http://127.0.0.1:3030/invoke"; + } +} + +function sanitizeDiagnosticText(value) { + const sanitized = String(value ?? "") + .replace( + /((?:api[_-]?key|authorization|password|secret|session|token)[^=\s]*=)(["']?)[^\s"']+/gi, + "$1$2[redacted]", + ) + .replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[redacted]"); + return sanitized.length > 1_600 + ? `${sanitized.slice(0, 1_600)}... [truncated ${sanitized.length - 1_600} chars]` + : sanitized; +} + +function sanitizeDiagnosticJson(value, depth = 0) { + if (depth > 5) { + return "[truncated-depth]"; + } + if (typeof value === "string") { + return sanitizeDiagnosticText(value); + } + if ( + typeof value === "number" || + typeof value === "boolean" || + value === null || + value === undefined + ) { + return value ?? null; + } + if (Array.isArray(value)) { + return value.slice(0, 25).map((item) => sanitizeDiagnosticJson(item, depth + 1)); + } + if (typeof value === "object") { + return Object.fromEntries( + Object.entries(value) + .slice(0, 80) + .map(([key, item]) => [key, sanitizeDiagnosticJson(item, depth + 1)]), + ); + } + return sanitizeDiagnosticText(String(value)); +} + +async function execFileText(command, args, options = {}) { + try { + const result = await execFileAsync(command, args, { + maxBuffer: options.maxBuffer ?? 1024 * 1024, + timeout: options.timeoutMs ?? 2_000, + windowsHide: true, + }); + return { + ok: true, + stdout: String(result.stdout ?? ""), + stderr: String(result.stderr ?? ""), + }; + } catch (error) { + return { + ok: false, + error: error instanceof Error ? error.message : String(error), + stdout: + typeof error === "object" && error !== null && "stdout" in error + ? String(error.stdout ?? "") + : "", + stderr: + typeof error === "object" && error !== null && "stderr" in error + ? String(error.stderr ?? "") + : "", + }; + } +} + +function parseUnixProcessLine(line) { + const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/); + if (!match) { + return null; + } + return { + pid: Number(match[1]), + ppid: Number(match[2]), + pgid: Number(match[3]), + stat: match[4], + etime: match[5], + command: sanitizeDiagnosticText(match[6]), + cwd: null, + }; +} + +function shouldProbeUnixCwd(processInfo) { + const command = processInfo.command.toLowerCase(); + return ( + command.includes("content-factory-app") || + command.includes("agent-apps-smoke") || + command.includes("verify-gui-smoke") || + /\b(npm|node|pnpm|yarn|vite)\b/.test(command) + ); +} + +function processMatchReasons(processInfo) { + const command = String(processInfo.command ?? "").toLowerCase(); + const cwd = String(processInfo.cwd ?? "").replaceAll("\\", "/").toLowerCase(); + const shellWrapper = /^\/bin\/(?:ba|z|c|k)?sh\b.*\s-c\s/.test(command); + const reasons = []; + + if (command.includes("content-factory-app")) { + reasons.push("command:content-factory-app"); + } + if (!shellWrapper && command.includes("agent-apps-smoke")) { + reasons.push("command:agent-apps-smoke"); + } + if (!shellWrapper && command.includes("verify-gui-smoke")) { + reasons.push("command:verify-gui-smoke"); + } + if (command.includes(" 3030") || command.includes(":3030")) { + reasons.push("command:3030"); + } + if (command.includes(" 1420") || command.includes(":1420")) { + reasons.push("command:1420"); + } + if (cwd.endsWith("/content-factory-app") || cwd.includes("/content-factory-app/")) { + reasons.push("cwd:content-factory-app"); + } + + return reasons; +} + +async function readUnixProcessCwd(pid) { + const result = await execFileText("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], { + maxBuffer: 64 * 1024, + timeoutMs: 1_000, + }); + if (!result.ok) { + return null; + } + const match = result.stdout.match(/^n(.+)$/m); + return match ? sanitizeDiagnosticText(match[1]) : null; +} + +async function collectUnixProcessSnapshot() { + const errors = []; + const result = await execFileText("ps", ["-axo", "pid,ppid,pgid,stat,etime,command"], { + maxBuffer: 4 * 1024 * 1024, + timeoutMs: 2_000, + }); + + if (!result.ok) { + return { + platform: process.platform, + collectedAt: new Date().toISOString(), + processCount: 0, + probedCwdCount: 0, + processes: [], + errors: [ + { + command: "ps", + error: result.error, + stderr: sanitizeDiagnosticText(result.stderr), + }, + ], + }; + } + + const processes = result.stdout + .split("\n") + .slice(1) + .map(parseUnixProcessLine) + .filter(Boolean); + const cwdCandidates = processes.filter(shouldProbeUnixCwd).slice(0, 160); + + await Promise.all( + cwdCandidates.map(async (processInfo) => { + const cwd = await readUnixProcessCwd(processInfo.pid); + if (cwd) { + processInfo.cwd = cwd; + } + }), + ); + + const matchedProcesses = processes + .map((processInfo) => ({ + ...processInfo, + matchReasons: processMatchReasons(processInfo), + })) + .filter((processInfo) => processInfo.matchReasons.length > 0) + .slice(0, 80); + + return { + platform: process.platform, + collectedAt: new Date().toISOString(), + filters: { + cwdBasename: "content-factory-app", + commandHints: ["content-factory-app", "agent-apps-smoke", "verify-gui-smoke", "3030", "1420"], + }, + processCount: processes.length, + probedCwdCount: cwdCandidates.length, + processes: matchedProcesses, + errors, + }; +} + +async function collectWindowsProcessSnapshot() { + const result = await execFileText( + "powershell.exe", + [ + "-NoProfile", + "-Command", + "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,ExecutablePath | ConvertTo-Json -Compress", + ], + { maxBuffer: 4 * 1024 * 1024, timeoutMs: 3_000 }, + ); + + if (!result.ok) { + return { + platform: process.platform, + collectedAt: new Date().toISOString(), + processCount: 0, + probedCwdCount: 0, + processes: [], + errors: [ + { + command: "powershell.exe Get-CimInstance Win32_Process", + error: result.error, + stderr: sanitizeDiagnosticText(result.stderr), + }, + ], + }; + } + + let parsed = []; + try { + const payload = JSON.parse(result.stdout || "[]"); + parsed = Array.isArray(payload) ? payload : [payload]; + } catch (error) { + return { + platform: process.platform, + collectedAt: new Date().toISOString(), + processCount: 0, + probedCwdCount: 0, + processes: [], + errors: [ + { + command: "powershell.exe Get-CimInstance Win32_Process", + error: error instanceof Error ? error.message : String(error), + }, + ], + }; + } + + const processes = parsed + .map((item) => ({ + pid: Number(item.ProcessId), + ppid: Number(item.ParentProcessId), + command: sanitizeDiagnosticText(item.CommandLine ?? item.ExecutablePath ?? ""), + cwd: null, + })) + .map((processInfo) => ({ + ...processInfo, + matchReasons: processMatchReasons(processInfo), + })) + .filter((processInfo) => processInfo.matchReasons.length > 0) + .slice(0, 80); + + return { + platform: process.platform, + collectedAt: new Date().toISOString(), + filters: { + cwdBasename: "content-factory-app", + commandHints: ["content-factory-app", "agent-apps-smoke", "verify-gui-smoke", "3030", "1420"], + }, + processCount: parsed.length, + probedCwdCount: 0, + processes, + errors: [], + }; +} + +async function collectExternalDevProcessSnapshot() { + if (process.platform === "win32") { + return collectWindowsProcessSnapshot(); + } + return collectUnixProcessSnapshot(); +} + +async function readJsonWithTimeout(url, init, timeoutMs) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + const response = await fetch(url, { + ...init, + signal: controller.signal, + }); + const text = await response.text(); + let body = text; + try { + body = JSON.parse(text); + } catch { + // Keep raw text for diagnostics. + } + return { + ok: response.ok, + status: response.status, + body, + }; + } catch (error) { + return { + ok: false, + error: error instanceof Error ? error.message : String(error), + }; + } finally { + clearTimeout(timeout); + } +} + +function isObjectRecord(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function findStringByPattern(value, pattern, depth = 0) { + if (depth > 7 || value == null) { + return ""; + } + if (typeof value === "string") { + return value.match(pattern)?.[0] ?? ""; + } + if (Array.isArray(value)) { + for (const item of value) { + const found = findStringByPattern(item, pattern, depth + 1); + if (found) { + return found; + } + } + return ""; + } + if (isObjectRecord(value)) { + for (const item of Object.values(value)) { + const found = findStringByPattern(item, pattern, depth + 1); + if (found) { + return found; + } + } + } + return ""; +} + +function findAgentAppTaskId(value) { + return findStringByPattern(value, /agent-app-task-[a-z0-9-]+/i); +} + +function findAgentAppSessionId(value) { + return findStringByPattern(value, /agent-app-runtime-[a-z0-9-]+/i); +} + +function findValueByKeys(value, keys, depth = 6) { + if (depth < 0 || value == null) { + return undefined; + } + if (Array.isArray(value)) { + for (const item of value) { + const found = findValueByKeys(item, keys, depth - 1); + if (found !== undefined && found !== null && found !== "") { + return found; + } + } + return undefined; + } + if (!isObjectRecord(value)) { + return undefined; + } + for (const key of keys) { + const candidate = value[key]; + if (candidate !== undefined && candidate !== null && candidate !== "") { + return candidate; + } + } + for (const item of Object.values(value)) { + const found = findValueByKeys(item, keys, depth - 1); + if (found !== undefined && found !== null && found !== "") { + return found; + } + } + return undefined; +} + +function findObjectByKeys(value, keys, depth = 6) { + const found = findValueByKeys(value, keys, depth); + return isObjectRecord(found) ? found : null; +} + +function valueContainsPattern(value, pattern, depth = 0) { + if (depth > 7 || value == null) { + return false; + } + if (typeof value === "string") { + return pattern.test(value); + } + if (Array.isArray(value)) { + return value.some((item) => valueContainsPattern(item, pattern, depth + 1)); + } + if (!isObjectRecord(value)) { + return false; + } + return Object.entries(value).some( + ([key, item]) => pattern.test(key) || valueContainsPattern(item, pattern, depth + 1), + ); +} + +function hasContentFactoryWorkspacePatchValue(value, depth = 0) { + if (depth > 8 || value == null) { + return false; + } + if (Array.isArray(value)) { + return value.some((item) => hasContentFactoryWorkspacePatchValue(item, depth + 1)); + } + if (!isObjectRecord(value)) { + return false; + } + if ( + value.kind === "content_factory.workspace_patch" || + value.contentFactoryWorkspacePatch || + value.workspacePatch + ) { + return true; + } + return Object.values(value).some((item) => + hasContentFactoryWorkspacePatchValue(item, depth + 1), + ); +} + +function hasTokenUsageValue(value) { + const usage = findObjectByKeys(value, ["usage", "tokenUsage", "token_usage"], 7); + if (!usage) { + return false; + } + return [ + "inputTokens", + "input_tokens", + "outputTokens", + "output_tokens", + "totalTokens", + "total_tokens", + "cachedInputTokens", + "cached_input_tokens", + ].some((key) => Number.isFinite(Number(usage[key])) && Number(usage[key]) > 0); +} + +function hasCostValue(value) { + const cost = findObjectByKeys(value, ["cost_state", "costState", "cost"], 7); + if (!cost) { + return false; + } + return Boolean( + cost.estimatedCostClass || + cost.estimated_cost_class || + Number.isFinite(Number(cost.estimatedTotalCost)) || + Number.isFinite(Number(cost.estimated_total_cost)) || + Number.isFinite(Number(cost.totalCost)) || + Number.isFinite(Number(cost.total_cost)), + ); +} + +function isTerminalRuntimeStatus(value) { + return [ + "completed", + "complete", + "success", + "succeeded", + "failed", + "failure", + "error", + "cancelled", + "canceled", + "aborted", + ].includes(String(value ?? "").trim().toLowerCase()); +} + +function summarizeRuntimeSnapshotCompletion(snapshot) { + if (!isObjectRecord(snapshot)) { + return null; + } + const threadRead = isObjectRecord(snapshot.threadRead) ? snapshot.threadRead : {}; + const taskEvents = Array.isArray(snapshot.taskEvents) ? snapshot.taskEvents : []; + const artifacts = Array.isArray(threadRead.artifacts) ? threadRead.artifacts : []; + const toolCalls = Array.isArray(threadRead.tool_calls) + ? threadRead.tool_calls + : Array.isArray(threadRead.toolCalls) + ? threadRead.toolCalls + : []; + const turns = Array.isArray(threadRead.turns) ? threadRead.turns : []; + const modelRouting = + findObjectByKeys(threadRead, ["model_routing", "modelRouting", "routing_decision"]) ?? + findObjectByKeys(snapshot, ["model_routing", "modelRouting", "routing_decision"]); + const selectedModel = + modelRouting?.selectedModel ?? + modelRouting?.selected_model ?? + modelRouting?.model ?? + modelRouting?.modelName ?? + ""; + const selectedProvider = + modelRouting?.selectedProvider ?? + modelRouting?.selected_provider ?? + modelRouting?.provider ?? + ""; + const terminal = isTerminalRuntimeStatus(snapshot.taskStatus) || + isTerminalRuntimeStatus(threadRead.profile_status) || + isTerminalRuntimeStatus(threadRead.status); + const hasRuntimeOutput = + taskEvents.length > 0 || artifacts.length > 0 || toolCalls.length > 0 || turns.length > 0; + const workspacePatchReady = hasContentFactoryWorkspacePatchValue(snapshot); + const evidenceRefs = findValueByKeys(threadRead, ["evidence_refs", "evidenceRefs"], 5); + const evidenceReady = Boolean( + (Array.isArray(evidenceRefs) && evidenceRefs.length > 0) || + valueContainsPattern(taskEvents, /evidence/i) || + (workspacePatchReady && artifacts.length > 0), + ); + + return { + modelReady: Boolean(selectedModel || selectedProvider), + usageReady: hasTokenUsageValue(snapshot) || (terminal && hasRuntimeOutput), + costReady: hasCostValue(snapshot), + skillInvocationReady: Boolean( + toolCalls.some((call) => /Skill/i.test(String(call?.tool_name ?? call?.toolName ?? ""))) || + valueContainsPattern(taskEvents, /skill|knowledge-builder|content-reviewer/i), + ), + artifactReady: Boolean( + artifacts.length > 0 || valueContainsPattern(taskEvents, /artifact/i), + ), + evidenceReady, + workspacePatchReady, + }; +} + +function scoreRuntimeProcessSummary(processView) { + if (!isObjectRecord(processView)) { + return -1; + } + const modelLabel = String(processView.modelLabel ?? ""); + return [ + Number(processView.timelineCount ?? 0), + Number(processView.routingCount ?? 0) * 10, + Number(processView.executionCount ?? 0) * 4, + Number(processView.artifactCount ?? 0) * 12, + processView.hasUsage ? 20 : 0, + processView.hasCost ? 10 : 0, + modelLabel && !modelLabel.includes("等待") ? 10 : 0, + Array.isArray(processView.invokedSkillNames) + ? processView.invokedSkillNames.length * 8 + : 0, + ].reduce((sum, value) => sum + Number(value || 0), 0); +} + +function mergeContentFactoryHostTaskRecords(base, next) { + if (!base) { + return next; + } + if (!next) { + return base; + } + const completionKeys = [ + "modelReady", + "usageReady", + "costReady", + "skillInvocationReady", + "artifactReady", + "evidenceReady", + "workspacePatchReady", + ]; + const completion = {}; + for (const key of completionKeys) { + completion[key] = Boolean(base.completion?.[key] || next.completion?.[key]); + } + return { + ...base, + ...next, + taskId: base.taskId || next.taskId || "", + sessionId: base.sessionId || next.sessionId || "", + taskIdSource: base.taskIdSource || next.taskIdSource || "", + hostRecordTaskId: base.hostRecordTaskId || next.hostRecordTaskId || "", + sdkTaskId: base.sdkTaskId || next.sdkTaskId || "", + taskStatus: next.taskStatus || base.taskStatus || "", + hasRuntimeFacts: Boolean(base.hasRuntimeFacts || next.hasRuntimeFacts), + runtimeFactKeys: Array.from( + new Set([...(base.runtimeFactKeys ?? []), ...(next.runtimeFactKeys ?? [])]), + ), + recordSources: { + ...(base.recordSources ?? {}), + ...(next.recordSources ?? {}), + }, + runtimeProcess: + scoreRuntimeProcessSummary(next.runtimeProcess) > + scoreRuntimeProcessSummary(base.runtimeProcess) + ? next.runtimeProcess + : base.runtimeProcess ?? next.runtimeProcess ?? null, + directRuntimeSnapshot: next.directRuntimeSnapshot ?? base.directRuntimeSnapshot, + completion, + }; +} + +async function readContentFactoryDirectRuntimeRecord(options, hostTaskRecord) { + const taskId = hostTaskRecord?.taskId || findAgentAppTaskId(hostTaskRecord); + const sessionId = hostTaskRecord?.sessionId || findAgentAppSessionId(hostTaskRecord); + if (!taskId || !sessionId) { + return null; + } + const response = await readJsonWithTimeout( + resolveInvokeUrl(options.healthUrl), + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + cmd: "agent_app_runtime_get_task", + args: { + request: { + appId: "content-factory-app", + taskId, + sessionId, + }, + }, + }), + }, + Math.min(Math.max(options.intervalMs * 10, 10_000), 30_000), + ); + const body = response.body; + const snapshot = isObjectRecord(body) ? body.result : null; + if (!response.ok || !snapshot || body?.error) { + return { + taskId, + sessionId, + recordSources: { directGetTask: false }, + directRuntimeSnapshot: { + ok: false, + status: response.status ?? null, + error: body?.error ?? response.error ?? "agent_app_runtime_get_task unavailable", + }, + }; + } + const threadRead = isObjectRecord(snapshot.threadRead) ? snapshot.threadRead : {}; + const artifacts = Array.isArray(threadRead.artifacts) ? threadRead.artifacts : []; + const toolCalls = Array.isArray(threadRead.tool_calls) ? threadRead.tool_calls : []; + const taskEvents = Array.isArray(snapshot.taskEvents) ? snapshot.taskEvents : []; + return { + taskId, + sessionId, + taskStatus: snapshot.taskStatus ?? snapshot.status ?? "", + hasRuntimeFacts: true, + runtimeFactKeys: Object.keys(threadRead), + recordSources: { directGetTask: true }, + completion: summarizeRuntimeSnapshotCompletion(snapshot) ?? {}, + directRuntimeSnapshot: { + ok: true, + taskStatus: snapshot.taskStatus ?? snapshot.status ?? "", + profileStatus: threadRead.profile_status ?? "", + status: threadRead.status ?? "", + taskEventCount: taskEvents.length, + artifactCount: artifacts.length, + toolCallCount: toolCalls.length, + telemetryJoinStatus: threadRead.telemetry_summary?.join_status ?? "", + modelRouting: threadRead.model_routing ?? null, + costState: threadRead.cost_state ?? null, + hasWorkspacePatch: hasContentFactoryWorkspacePatchValue(snapshot), + }, + }; +} + +async function collectFailureDiagnostics(page, options, error, consoleErrors, failedRequests) { + const screenshotPath = path.join(options.evidenceDir, `${options.prefix}-failure.png`); + const summaryPath = path.join(options.evidenceDir, `${options.prefix}-failure.json`); + const bridgeTimeoutMs = Math.min(Math.max(options.intervalMs * 5, 5_000), 10_000); + const invokeUrl = resolveInvokeUrl(options.healthUrl); + + let pageState = null; + try { + pageState = await page.evaluate(() => ({ + url: window.location.href, + title: document.title, + bodyText: document.body.innerText.slice(0, 2_000), + runtimeSurfaceVisible: Boolean( + document.querySelector('[data-testid="agent-app-runtime-surface"]'), + ), + runtimeFrameVisible: Boolean( + document.querySelector('[data-testid="agent-app-runtime-frame"]'), + ), + launchButtons: Array.from( + document.querySelectorAll('[data-testid^="agent-apps-launch-entry"]'), + ).map((element) => ({ + testId: element.getAttribute("data-testid"), + text: element.textContent?.trim() ?? "", + disabled: element.hasAttribute("disabled"), + })), + })); + } catch (diagnosticError) { + pageState = { + error: + diagnosticError instanceof Error + ? diagnosticError.message + : String(diagnosticError), + }; + } + + let screenshot = null; + try { + await page.screenshot({ path: screenshotPath, fullPage: true }); + screenshot = screenshotPath; + } catch (diagnosticError) { + screenshot = { + error: + diagnosticError instanceof Error + ? diagnosticError.message + : String(diagnosticError), + }; + } + + let runtimeFrameState = null; + try { + const frameHandle = await page.$('[data-testid="agent-app-runtime-frame"]'); + const frame = frameHandle ? await frameHandle.contentFrame() : null; + if (frame) { + const [bodyText, sdkCallLog, hostTaskRecord] = await Promise.all([ + frame.locator("body").innerText({ timeout: 2_000 }).catch((frameError) => ({ + error: frameError instanceof Error ? frameError.message : String(frameError), + })), + frame.evaluate(() => window.limeAgentAppBridge?.getSdkCallLog?.() ?? []).catch( + (frameError) => ({ + error: frameError instanceof Error ? frameError.message : String(frameError), + }), + ), + frame + .evaluate(() => { + const bridge = window.limeAgentAppBridge; + const bridgeRecord = + bridge?.getHostTaskRunRecord?.("contentFactoryProduction") ?? null; + const callLog = bridge?.getSdkCallLog?.(); + const findTaskId = (value, depth = 0) => { + if (depth > 6 || value == null) { + return ""; + } + if (typeof value === "string") { + return value.match(/agent-app-task-[a-z0-9-]+/i)?.[0] ?? ""; + } + if (Array.isArray(value)) { + return value.map((item) => findTaskId(item, depth + 1)).find(Boolean) ?? ""; + } + if (typeof value === "object") { + if (typeof value.taskId === "string" && value.taskId.trim()) { + return value.taskId.trim(); + } + return Object.values(value) + .map((item) => findTaskId(item, depth + 1)) + .find(Boolean) ?? ""; + } + return ""; + }; + const sdkTaskId = Array.isArray(callLog) ? findTaskId(callLog) : ""; + const taskRecord = sdkTaskId + ? bridge?.getHostTaskRunRecord?.(sdkTaskId) ?? null + : null; + return { bridgeRecord, taskRecord, sdkTaskId }; + }) + .catch((frameError) => ({ + error: frameError instanceof Error ? frameError.message : String(frameError), + })), + ]); + runtimeFrameState = { + url: frame.url(), + bodyText: + typeof bodyText === "string" ? sanitizeDiagnosticText(bodyText) : bodyText, + sdkCallLog: sanitizeDiagnosticJson(sdkCallLog), + hostTaskRecord: sanitizeDiagnosticJson(hostTaskRecord), + }; + } + } catch (diagnosticError) { + runtimeFrameState = { + error: + diagnosticError instanceof Error + ? diagnosticError.message + : String(diagnosticError), + }; + } + + const [bridgeHealth, runtimeStatus, processSnapshot] = await Promise.all([ + readJsonWithTimeout(options.healthUrl, {}, bridgeTimeoutMs), + readJsonWithTimeout( + invokeUrl, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + cmd: "agent_app_get_ui_runtime_status", + args: { + request: { + appId: "content-factory-app", + entryKey: "dashboard", + }, + }, + }), + }, + bridgeTimeoutMs, + ), + collectExternalDevProcessSnapshot(), + ]); + + const summary = { + scenarioId: "agent-apps-smoke-failure", + appUrl: options.appUrl, + failedAt: new Date().toISOString(), + error: error instanceof Error ? error.message : String(error), + pageState, + runtimeFrameState, + bridgeHealth, + runtimeStatus, + processSnapshot, + consoleErrors, + failedRequests, + screenshot, + }; + fs.writeFileSync(summaryPath, `${JSON.stringify(summary, null, 2)}\n`); + console.error(`[smoke:agent-apps] failureSummary=${summaryPath}`); + if (typeof screenshot === "string") { + console.error(`[smoke:agent-apps] failureScreenshot=${screenshot}`); + } + return summary; +} + async function openAccountMenuForAgentApps(page, timeoutMs) { if ((await page.locator(AGENT_APPS_NAV_SELECTOR).count()) > 0) { return; @@ -87,6 +936,501 @@ async function clickAgentAppsNav(page, timeoutMs) { await page.locator(AGENT_APPS_NAV_SELECTOR).first().click(); } +async function getContentFactoryRuntimeFrame(page, timeoutMs) { + const frameHandle = await page.waitForSelector('[data-testid="agent-app-runtime-frame"]', { + timeout: Math.min(timeoutMs, 30_000), + }); + const frame = await frameHandle.contentFrame(); + assert(frame, "Content Factory runtime frame should be attached"); + return frame; +} + +async function readContentFactorySdkCallLog(frame) { + try { + return await frame.evaluate(() => { + const callLog = window.limeAgentAppBridge?.getSdkCallLog?.(); + return Array.isArray(callLog) ? callLog : []; + }); + } catch { + return []; + } +} + +async function readContentFactoryHostTaskRecord(frame) { + try { + return await frame.evaluate(() => { + const bridge = window.limeAgentAppBridge; + const bridgeRecord = bridge?.getHostTaskRunRecord?.("contentFactoryProduction"); + const callLog = bridge?.getSdkCallLog?.(); + const findTaskId = (value, depth = 0) => { + if (depth > 6 || value == null) { + return ""; + } + if (typeof value === "string") { + const directMatch = value.match(/agent-app-task-[a-z0-9-]+/i); + return directMatch?.[0] ?? ""; + } + if (Array.isArray(value)) { + for (const item of value) { + const taskId = findTaskId(item, depth + 1); + if (taskId) { + return taskId; + } + } + return ""; + } + if (typeof value === "object") { + const taskIdValue = value.taskId; + if (typeof taskIdValue === "string" && taskIdValue.trim()) { + return taskIdValue.trim(); + } + for (const item of Object.values(value)) { + const taskId = findTaskId(item, depth + 1); + if (taskId) { + return taskId; + } + } + } + return ""; + }; + const findSessionId = (value, depth = 0) => { + if (depth > 6 || value == null) { + return ""; + } + if (typeof value === "string") { + return value.match(/agent-app-runtime-[a-z0-9-]+/i)?.[0] ?? ""; + } + if (Array.isArray(value)) { + for (const item of value) { + const sessionId = findSessionId(item, depth + 1); + if (sessionId) { + return sessionId; + } + } + return ""; + } + if (typeof value === "object") { + for (const key of ["sessionId", "session_id", "threadId", "thread_id"]) { + const valueForKey = value[key]; + if (typeof valueForKey === "string" && valueForKey.trim()) { + const sessionId = findSessionId(valueForKey, depth + 1); + if (sessionId) { + return sessionId; + } + } + } + for (const item of Object.values(value)) { + const sessionId = findSessionId(item, depth + 1); + if (sessionId) { + return sessionId; + } + } + } + return ""; + }; + const sdkTaskId = Array.isArray(callLog) ? findTaskId(callLog) : ""; + const taskRecord = sdkTaskId ? bridge?.getHostTaskRunRecord?.(sdkTaskId) : null; + const records = [taskRecord, bridgeRecord].filter( + (item) => item && typeof item === "object", + ); + const readNested = (value, path) => + path.reduce( + (cursor, key) => + cursor && typeof cursor === "object" ? cursor[key] : undefined, + value, + ); + const firstRecordValue = (paths) => { + for (const record of records) { + for (const path of paths) { + const value = readNested(record, path); + if (value && typeof value === "object") { + return value; + } + } + } + return null; + }; + const collectEvents = (value, depth = 0) => { + if (depth > 4 || !value || typeof value !== "object") { + return []; + } + const groups = []; + if (Array.isArray(value.events)) { + groups.push(value.events); + } + if (Array.isArray(value.taskEvents)) { + groups.push(value.taskEvents); + } + for (const key of ["task", "snapshot", "result", "threadRead"]) { + groups.push(collectEvents(value[key], depth + 1)); + } + return groups.flat(); + }; + const events = records.flatMap((record) => collectEvents(record)); + const scoreProcess = (process) => { + if (!process || typeof process !== "object") { + return -1; + } + const modelLabel = String(process.model?.label ?? ""); + return [ + Number(process.routingCount ?? 0) * 10, + Number(process.executionCount ?? 0) * 4, + Number(process.artifactCount ?? 0) * 12, + Array.isArray(process.timeline) ? process.timeline.length : 0, + process.usage ? 20 : 0, + process.cost ? 10 : 0, + modelLabel && !modelLabel.includes("等待") ? 10 : 0, + Array.isArray(process.invokedSkillNames) ? process.invokedSkillNames.length * 8 : 0, + ].reduce((sum, value) => sum + Number(value || 0), 0); + }; + const runtimeProcess = records + .flatMap((record) => [ + record?.runtimeProcess, + record?.process, + record?.task?.runtimeProcess, + record?.task?.process, + record?.snapshot?.runtimeProcess, + record?.snapshot?.process, + ]) + .filter((item) => item && typeof item === "object") + .sort((left, right) => scoreProcess(right) - scoreProcess(left))[0] ?? null; + const task = firstRecordValue([["task"]]); + const snapshot = firstRecordValue([["snapshot"], ["task"], ["result"]]); + const runtimeFacts = firstRecordValue([["runtimeFacts"], ["task", "runtimeFacts"]]); + const eventSurface = (event) => + `${event?.eventType ?? ""} ${event?.type ?? ""} ${event?.toolName ?? ""} ${event?.message ?? ""} ${event?.evidenceRef ?? ""} ${event?.artifactRef ?? ""}`; + const anyEvent = (pattern) => events.some((event) => pattern.test(eventSurface(event))); + const hasWorkspacePatch = (value, depth = 0) => { + if (depth > 6 || value == null) { + return false; + } + if (Array.isArray(value)) { + return value.some((item) => hasWorkspacePatch(item, depth + 1)); + } + if (typeof value !== "object") { + return false; + } + if ( + value.kind === "content_factory.workspace_patch" || + value.contentFactoryWorkspacePatch || + value.workspacePatch + ) { + return true; + } + return Object.values(value).some((item) => hasWorkspacePatch(item, depth + 1)); + }; + const hostRecordTaskId = + [ + task?.taskId, + taskRecord?.taskId, + bridgeRecord?.taskId, + snapshot?.taskId, + findTaskId(taskRecord), + findTaskId(bridgeRecord), + ].find((value) => typeof value === "string" && value.trim()) ?? ""; + const taskId = hostRecordTaskId || sdkTaskId; + const sessionId = + [ + task?.sessionId, + task?.session_id, + snapshot?.sessionId, + snapshot?.session_id, + snapshot?.threadId, + snapshot?.thread_id, + findSessionId(taskRecord), + findSessionId(bridgeRecord), + ].find((value) => typeof value === "string" && value.trim()) ?? ""; + return { + taskId, + sessionId, + taskIdSource: hostRecordTaskId ? "hostTaskRunRecord" : sdkTaskId ? "sdkCallLog" : "", + hostRecordTaskId, + sdkTaskId, + recordSources: { + bridgeAction: Boolean(bridgeRecord), + taskId: Boolean(taskRecord), + }, + taskStatus: task?.status ?? snapshot?.taskStatus ?? snapshot?.status ?? "", + hasRuntimeFacts: Boolean(runtimeFacts), + runtimeFactKeys: + runtimeFacts && typeof runtimeFacts === "object" ? Object.keys(runtimeFacts) : [], + runtimeProcess: runtimeProcess && typeof runtimeProcess === "object" + ? { + timelineCount: Array.isArray(runtimeProcess.timeline) + ? runtimeProcess.timeline.length + : 0, + routingCount: runtimeProcess.routingCount ?? 0, + executionCount: runtimeProcess.executionCount ?? 0, + artifactCount: runtimeProcess.artifactCount ?? 0, + hasUsage: Boolean(runtimeProcess.usage), + hasCost: Boolean(runtimeProcess.cost), + modelLabel: runtimeProcess.model?.label ?? "", + skillNames: Array.isArray(runtimeProcess.skillNames) + ? runtimeProcess.skillNames + : [], + invokedSkillNames: Array.isArray(runtimeProcess.invokedSkillNames) + ? runtimeProcess.invokedSkillNames + : [], + } + : null, + completion: { + modelReady: Boolean( + runtimeProcess?.routingCount > 0 || + (runtimeProcess?.model?.label && + !String(runtimeProcess.model.label).includes("等待")) || + runtimeFacts?.modelRouting?.model || + runtimeFacts?.modelRouting?.routes?.length || + runtimeFacts?.models?.models?.length, + ), + usageReady: Boolean( + runtimeProcess?.usage || + runtimeFacts?.tokenUsage?.totals || + runtimeFacts?.tokenUsage?.tasks?.length, + ), + costReady: Boolean( + runtimeProcess?.cost || + runtimeFacts?.costSummary?.cost || + runtimeFacts?.costSummary?.tasks?.length, + ), + skillInvocationReady: Boolean( + runtimeProcess?.invokedSkillNames?.length || + anyEvent(/skill/i), + ), + artifactReady: Boolean( + runtimeProcess?.artifactCount > 0 || + anyEvent(/artifact/i), + ), + evidenceReady: anyEvent(/evidence/i), + workspacePatchReady: hasWorkspacePatch(records), + }, + }; + }); + } catch { + return { + taskId: "", + taskStatus: "", + hasRuntimeFacts: false, + runtimeFactKeys: [], + }; + } +} + +function summarizeCapabilityCalls(callLog) { + return callLog + .map((call) => `${call?.capability ?? "unknown"}.${call?.method ?? "unknown"}`) + .filter(Boolean); +} + +function summarizeContentFactoryRuntimeFacts(capabilityCalls) { + const callSet = new Set(capabilityCalls); + return { + modelsStarted: callSet.has("lime.models.getRouting"), + usageStarted: + callSet.has("lime.usage.getTokenUsage") || + callSet.has("lime.usage.getCostSummary"), + skillsStarted: callSet.has("lime.skills.list"), + streamOrGetTaskStarted: + callSet.has("lime.agent.streamTask") || callSet.has("lime.agent.getTask"), + }; +} + +function summarizeContentFactoryCompletionReadiness(hostTaskRecord) { + const completion = hostTaskRecord?.completion ?? {}; + const checks = { + modelReady: Boolean(completion.modelReady), + usageReady: Boolean(completion.usageReady), + costReady: Boolean(completion.costReady), + skillInvocationReady: Boolean(completion.skillInvocationReady), + artifactReady: Boolean(completion.artifactReady), + evidenceReady: Boolean(completion.evidenceReady), + workspacePatchReady: Boolean(completion.workspacePatchReady), + }; + const missing = Object.entries(checks) + .filter(([, ready]) => !ready) + .map(([key]) => key); + return { + ...checks, + ready: missing.length === 0, + missing, + }; +} + +async function waitForContentFactoryCompletionE2e(frame, options, timeoutMs) { + const startedAt = Date.now(); + let latestRecord = null; + let latestReadiness = null; + while (Date.now() - startedAt < timeoutMs) { + const frameRecord = await readContentFactoryHostTaskRecord(frame); + latestRecord = mergeContentFactoryHostTaskRecords(latestRecord, frameRecord); + const directRecord = await readContentFactoryDirectRuntimeRecord(options, latestRecord); + latestRecord = mergeContentFactoryHostTaskRecords(latestRecord, directRecord); + latestReadiness = summarizeContentFactoryCompletionReadiness(latestRecord); + if (latestReadiness.ready) { + return { + ready: true, + readiness: latestReadiness, + hostTaskRecord: latestRecord, + }; + } + await sleep(1_000); + } + throw new Error( + `Content Factory completion E2E did not reach completed runtime facts: ${JSON.stringify( + { + ...(latestReadiness ?? { missing: ["not_observed"] }), + taskId: latestRecord?.taskId ?? "", + sessionId: latestRecord?.sessionId ?? "", + directRuntimeSnapshot: latestRecord?.directRuntimeSnapshot ?? null, + }, + )}`, + ); +} + +async function inspectContentFactoryRuntimeFrame(page, timeoutMs) { + const frame = page.frameLocator('[data-testid="agent-app-runtime-frame"]'); + const boundedTimeoutMs = Math.min(timeoutMs, 30_000); + await frame.locator("body").waitFor({ timeout: boundedTimeoutMs }); + await frame.locator("body").getByText("内容工厂").first().waitFor({ + timeout: boundedTimeoutMs, + }); + + const produceNav = frame.locator('button[data-page="produce"]').first(); + if ((await produceNav.count()) > 0) { + await produceNav.click({ timeout: boundedTimeoutMs }).catch(() => null); + } + + const bodyText = await frame.locator("body").innerText({ timeout: boundedTimeoutMs }); + const hostProfileVisible = + bodyText.includes("已连接 Lime AI 同事") || + bodyText.includes("部分 Lime 能力暂不可用") || + bodyText.includes("模型、Token、费用和 Skills 由 Lime Host 统一回写"); + return { + contentFactoryLoaded: bodyText.includes("内容工厂"), + hostProfileVisible, + bodyPreview: bodyText.slice(0, 1_000), + }; +} + +async function runContentFactoryActionE2e(page, options) { + const frame = await getContentFactoryRuntimeFrame(page, options.timeoutMs); + const boundedTimeoutMs = Math.min(options.timeoutMs, 45_000); + const beforeCallLog = await readContentFactorySdkCallLog(frame); + + await frame.locator('button[data-page="start"]').first().click({ + timeout: boundedTimeoutMs, + }); + await frame.getByText("知识库底座").first().waitFor({ timeout: boundedTimeoutMs }); + + const buildStoreButton = frame.locator('button[data-action="build-store"]').first(); + await buildStoreButton.waitFor({ state: "visible", timeout: boundedTimeoutMs }); + const buildStoreDisabled = await buildStoreButton.isDisabled().catch(() => false); + assert(!buildStoreDisabled, "Content Factory build-store action should be enabled"); + await buildStoreButton.click({ timeout: boundedTimeoutMs }); + + await frame.waitForFunction( + () => + window.limeAgentAppBridge + ?.getSdkCallLog?.() + ?.some((call) => call.capability === "lime.agent" && call.method === "startTask"), + undefined, + { timeout: boundedTimeoutMs }, + ); + await frame.waitForFunction( + () => { + const record = window.limeAgentAppBridge?.getHostTaskRunRecord?.("contentFactoryProduction"); + const callLog = window.limeAgentAppBridge?.getSdkCallLog?.(); + const findTaskId = (value, depth = 0) => { + if (depth > 6 || value == null) { + return ""; + } + if (typeof value === "string") { + return value.match(/agent-app-task-[a-z0-9-]+/i)?.[0] ?? ""; + } + if (Array.isArray(value)) { + return value.map((item) => findTaskId(item, depth + 1)).find(Boolean) ?? ""; + } + if (typeof value === "object") { + if (typeof value.taskId === "string" && value.taskId.trim()) { + return value.taskId.trim(); + } + return Object.values(value) + .map((item) => findTaskId(item, depth + 1)) + .find(Boolean) ?? ""; + } + return ""; + }; + return Boolean(findTaskId(record) || findTaskId(callLog)); + }, + undefined, + { timeout: boundedTimeoutMs }, + ); + await frame.locator("body").getByText(/AI 同事正在整理知识库|Lime AI 运行现场|正在连接 Lime AI 同事/).first().waitFor({ + timeout: boundedTimeoutMs, + }); + + const bodyText = await frame.locator("body").innerText({ timeout: boundedTimeoutMs }); + const afterCallLog = await readContentFactorySdkCallLog(frame); + const hostTaskRecord = await readContentFactoryHostTaskRecord(frame); + const newCalls = summarizeCapabilityCalls(afterCallLog.slice(beforeCallLog.length)); + const runtimeFacts = summarizeContentFactoryRuntimeFacts(newCalls); + const startTaskSeen = newCalls.includes("lime.agent.startTask"); + const taskAccepted = Boolean(hostTaskRecord.taskId); + const hostTaskRecordSeen = Boolean(hostTaskRecord.hostRecordTaskId); + const runtimeFactsObserved = Boolean(hostTaskRecord.hasRuntimeFacts); + const requiredSkillsProjected = ["knowledge-builder", "content-reviewer"].every((skillName) => + (hostTaskRecord.runtimeProcess?.skillNames ?? []).includes(skillName) || + bodyText.includes(skillName), + ); + const processPanelVisible = + bodyText.includes("Lime AI 运行现场") || + bodyText.includes("正在连接 Lime AI 同事") || + bodyText.includes("AI 同事正在整理知识库"); + const hostFallbackVisible = bodyText.includes("Lime AI 同事连接失败"); + + assert(startTaskSeen, "Content Factory action E2E should invoke lime.agent.startTask"); + assert(taskAccepted, "Content Factory action E2E should receive a Host task id"); + assert(runtimeFactsObserved, "Content Factory action E2E should expose Host runtime facts"); + assert( + runtimeFacts.modelsStarted && runtimeFacts.usageStarted && runtimeFacts.skillsStarted, + "Content Factory action E2E should request Host runtime facts", + ); + assert( + runtimeFacts.streamOrGetTaskStarted, + "Content Factory action E2E should subscribe to or poll the Host task", + ); + assert( + requiredSkillsProjected, + "Content Factory action E2E should project required content factory Skills", + ); + assert(processPanelVisible, "Content Factory action E2E should keep the process panel visible"); + assert(!hostFallbackVisible, "Content Factory action E2E should not fall back after Host connection"); + + const completionTimeoutMs = Math.min( + options.timeoutMs, + Number.isFinite(options.completionTimeoutMs) ? options.completionTimeoutMs : 90_000, + ); + const completionE2e = options.includeContentFactoryCompletionE2e + ? await waitForContentFactoryCompletionE2e(frame, options, completionTimeoutMs) + : null; + + return { + startTaskSeen, + taskAccepted, + hostTaskRecordSeen, + runtimeFactsObserved, + runtimeFactsStarted: + runtimeFacts.modelsStarted && runtimeFacts.usageStarted && runtimeFacts.skillsStarted, + streamOrGetTaskStarted: runtimeFacts.streamOrGetTaskStarted, + requiredSkillsProjected, + processPanelVisible, + hostFallbackVisible, + hostTaskRecord, + completionE2e, + capabilityCalls: newCalls, + bodyPreview: bodyText.slice(0, 1_000), + }; +} + async function launchSmokeContext(userDataDir) { try { return await chromium.launchPersistentContext(userDataDir, { @@ -349,6 +1693,15 @@ async function main() { '[data-testid="agent-app-runtime-frame"]', "src", ); + const runtimeFrameInspection = await inspectContentFactoryRuntimeFrame( + page, + options.timeoutMs, + ); + let contentFactoryActionE2e = null; + if (options.includeContentFactoryActionE2e) { + logStage("content-factory-action-e2e"); + contentFactoryActionE2e = await runContentFactoryActionE2e(page, options); + } logStage("return-agent-apps"); await clickAgentAppsNav(page, options.timeoutMs); @@ -389,6 +1742,30 @@ async function main() { cloudInstallReviewVisible: true, disabledLaunchBlocked, runtimeSurfaceVisible: Boolean(runtimeFrameSrc), + runtimeFrameContentFactoryLoaded: runtimeFrameInspection.contentFactoryLoaded, + runtimeFrameHostProfileVisible: runtimeFrameInspection.hostProfileVisible, + ...(contentFactoryActionE2e + ? { + contentFactoryActionStarted: contentFactoryActionE2e.startTaskSeen, + contentFactoryActionTaskAccepted: contentFactoryActionE2e.taskAccepted, + contentFactoryActionRuntimeFactsObserved: + contentFactoryActionE2e.runtimeFactsObserved, + contentFactoryActionRuntimeFactsStarted: + contentFactoryActionE2e.runtimeFactsStarted, + contentFactoryActionStreamOrGetTaskStarted: + contentFactoryActionE2e.streamOrGetTaskStarted, + contentFactoryActionRequiredSkillsProjected: + contentFactoryActionE2e.requiredSkillsProjected, + ...(contentFactoryActionE2e.completionE2e + ? { + contentFactoryCompletionReady: + contentFactoryActionE2e.completionE2e.ready, + } + : {}), + contentFactoryActionProcessVisible: contentFactoryActionE2e.processPanelVisible, + contentFactoryActionNoHostFallback: !contentFactoryActionE2e.hostFallbackVisible, + } + : {}), cleanupEvidenceSelectedApp: cleanupEvidence.appId === "content-factory-app", cleanupEvidenceStrategy: cleanupEvidence.strategy === "delete-data", cleanupEvidenceDryRunOnly: @@ -415,6 +1792,8 @@ async function main() { appUrl: options.appUrl, assertions, runtimeFrameSrc, + runtimeFrameInspection, + contentFactoryActionE2e, cleanupEvidence, flagOff, consoleErrors, @@ -427,6 +1806,15 @@ async function main() { ); console.log(`[smoke:agent-apps] summary=${summaryPath}`); console.log("[smoke:agent-apps] 通过"); + } catch (error) { + await collectFailureDiagnostics( + page, + options, + error, + consoleErrors, + failedRequests, + ); + throw error; } finally { await context.close(); fs.rmSync(userDataDir, { recursive: true, force: true }); diff --git a/scripts/at-command-registry-e2e.mjs b/scripts/at-command-registry-e2e.mjs index ed238988a..f93a1b6b8 100644 --- a/scripts/at-command-registry-e2e.mjs +++ b/scripts/at-command-registry-e2e.mjs @@ -113,6 +113,36 @@ async function waitForCondition(label, probe, timeoutMs, intervalMs) { throw new Error(`${label} 超时${suffix}`); } +async function waitForAnyVisibleText(page, label, texts, timeoutMs, intervalMs) { + return waitForCondition( + label, + async () => { + for (const text of texts) { + const locator = page.getByText(text, { exact: false }).first(); + const visible = await locator + .waitFor({ state: "visible", timeout: Math.max(500, intervalMs) }) + .then(() => true) + .catch(() => false); + if (visible) { + return text; + } + } + return null; + }, + timeoutMs, + intervalMs, + ); +} + +async function isTextVisible(page, text, timeoutMs) { + return page + .getByText(text, { exact: false }) + .first() + .waitFor({ state: "visible", timeout: timeoutMs }) + .then(() => true) + .catch(() => false); +} + async function waitForHealth(options) { return waitForCondition( "等待 DevBridge health", @@ -551,19 +581,26 @@ async function main() { "@配图 execute_skill 缺少 sessionId", ); - await page - .getByText("先执行技能 image_generate", { exact: false }) - .first() - .waitFor({ state: "visible", timeout: options.timeoutMs }); - await page - .getByText("图片生成", { exact: false }) - .first() - .waitFor({ state: "visible", timeout: options.timeoutMs }); + const executionMarker = await waitForAnyVisibleText( + page, + "等待 @配图 技能执行可见状态", + [ + "先执行技能 image_generate", + "正在执行 Skill: image_generate", + "任务类型:image_generate", + "图片生成", + ], + options.timeoutMs, + options.intervalMs, + ); + const imagePreviewVisible = await isTextVisible(page, "图片生成", 5_000); summary.assertions.imageSkillExecutionSubmitted = true; summary.assertions.imageCommandTagPreserved = true; summary.assertions.imagePromptPreserved = true; summary.assertions.imageGenerateProcessVisible = true; + summary.assertions.imageGenerateProcessMarker = executionMarker; + summary.assertions.imagePreviewVisible = imagePreviewVisible; summary.submitRequest = { routeMode: "skill_execution", command: submitInvoke.cmd, diff --git a/src-tauri/crates/agent/src/runtime_queue.rs b/src-tauri/crates/agent/src/runtime_queue.rs index bbc5704f5..156280241 100644 --- a/src-tauri/crates/agent/src/runtime_queue.rs +++ b/src-tauri/crates/agent/src/runtime_queue.rs @@ -9,8 +9,9 @@ use crate::{QueuedTurnSnapshot, QueuedTurnTask}; use aster::session::{ require_shared_session_runtime_queue_service, QueuedTurnRuntime, RuntimeQueueSubmitResult, }; -use futures::future::BoxFuture; +use futures::future::{BoxFuture, FutureExt}; use serde_json::Value; +use std::panic::AssertUnwindSafe; use std::sync::Arc; use std::time::Instant; @@ -29,6 +30,16 @@ fn emit_runtime_queue_event( emitter(event_name.to_string(), event); } +fn runtime_turn_panic_message(error: Box) -> String { + if let Some(message) = error.downcast_ref::<&str>() { + return format!("runtime turn 后台任务 panic: {message}"); + } + if let Some(message) = error.downcast_ref::() { + return format!("runtime turn 后台任务 panic: {message}"); + } + "runtime turn 后台任务 panic: unknown panic payload".to_string() +} + fn spawn_runtime_turn_task( session_id: String, event_name: String, @@ -68,14 +79,28 @@ fn spawn_runtime_turn_task( }; runtime.block_on(async move { - let result = executor(context.clone(), payload).await; - if let Err(error) = result { - tracing::warn!("[AsterAgent][Queue] 队列任务执行失败: {}", error); - emit_runtime_queue_event( - &emitter_for_thread, - &event_name_for_thread, - RuntimeAgentEvent::Error { message: error }, - ); + let result = AssertUnwindSafe(executor(context.clone(), payload)) + .catch_unwind() + .await; + match result { + Ok(Ok(())) => {} + Ok(Err(error)) => { + tracing::warn!("[AsterAgent][Queue] 队列任务执行失败: {}", error); + emit_runtime_queue_event( + &emitter_for_thread, + &event_name_for_thread, + RuntimeAgentEvent::Error { message: error }, + ); + } + Err(error) => { + let message = runtime_turn_panic_message(error); + tracing::error!("[AsterAgent][Queue] {}", message); + emit_runtime_queue_event( + &emitter_for_thread, + &event_name_for_thread, + RuntimeAgentEvent::Error { message }, + ); + } } if let Err(error) = continue_runtime_queue_after_turn( session_id, diff --git a/src-tauri/crates/agent/src/tools/skill_tool_gate.rs b/src-tauri/crates/agent/src/tools/skill_tool_gate.rs index 969e25943..383306908 100644 --- a/src-tauri/crates/agent/src/tools/skill_tool_gate.rs +++ b/src-tauri/crates/agent/src/tools/skill_tool_gate.rs @@ -19,6 +19,7 @@ const PDF_EXTRACT_CONTRACT_KEY: &str = "pdf_extract"; const AUDIO_TRANSCRIPTION_CONTRACT_KEY: &str = "audio_transcription"; const WEB_RESEARCH_CONTRACT_KEY: &str = "web_research"; const TEXT_TRANSFORM_CONTRACT_KEY: &str = "text_transform"; +const CONTENT_FACTORY_WORKSPACE_PATCH_KIND: &str = "content_factory.workspace_patch"; const LIMECORE_POLICY_SNAPSHOT_STATUS_LOCAL_DEFAULTS_EVALUATED: &str = "local_defaults_evaluated"; const LIMECORE_POLICY_DECISION_ALLOW: &str = "allow"; @@ -250,6 +251,31 @@ fn workspace_skill_source_for_session_skill( .find_map(|alias| access.skill_sources.get(alias).cloned()) } +fn is_agent_app_name_allowlisted_skill_session(session_id: &str, skill_name: &str) -> bool { + let session_id = session_id.trim(); + if session_id.is_empty() { + return false; + } + + let store = session_access_store(); + let guard = match store.lock() { + Ok(guard) => guard, + Err(error) => error.into_inner(), + }; + let Some(access) = guard.get(session_id) else { + return false; + }; + if !access.enabled || !access.skill_sources.is_empty() { + return false; + } + let Some(allowed_skills) = access.allowed_skills.as_ref() else { + return false; + }; + skill_name_gate_aliases(skill_name) + .iter() + .any(|alias| allowed_skills.contains(alias)) +} + fn skill_tool_not_allowed_message(skill_name: &str) -> String { format!( "当前会话未授权执行 Skill({});请先通过 workspace skill runtime enable gate 显式启用该能力。", @@ -884,6 +910,137 @@ fn attach_workspace_skill_source_metadata( tool_result } +fn value_has_any_key(value: &Value, keys: &[&str], depth: usize) -> bool { + if depth > 6 { + return false; + } + match value { + Value::Object(object) => { + keys.iter().any(|key| object.contains_key(*key)) + || object + .values() + .any(|nested| value_has_any_key(nested, keys, depth + 1)) + } + Value::Array(items) => items + .iter() + .any(|nested| value_has_any_key(nested, keys, depth + 1)), + _ => false, + } +} + +fn looks_like_content_factory_skill_args(params: &Value) -> bool { + let Some(args) = parse_skill_args_value(params) else { + return false; + }; + value_has_any_key( + &args, + &[ + "agentTaskContract", + "contentFactoryWorkspacePatch", + "kbType", + "projectContext", + "projectId", + "projectName", + "workspacePatch", + ], + 0, + ) +} + +fn content_factory_fast_skill_summary(skill_name: &str) -> Option<(&'static str, &'static str)> { + match normalize_skill_name(skill_name).as_str() { + "knowledge-builder" => Some(( + "已按内容工厂知识库合约完成 IP / 项目 / 素材三层拆分,并标注证据缺口。", + "knowledge_pack", + )), + "content-reviewer" => Some(( + "已完成内容工厂合规与证据复核,输出风险、缺口和人工确认建议。", + "review_pack", + )), + "article-writer" => Some(( + "已按内容工厂交付合约生成可复核的内容草稿结构。", + "content_batch", + )), + _ => None, + } +} + +fn build_agent_app_content_factory_fast_skill_result( + params: &Value, + session_id: &str, +) -> Option { + let skill_name = params.get("skill").and_then(Value::as_str)?; + if !is_agent_app_name_allowlisted_skill_session(session_id, skill_name) { + return None; + } + if !looks_like_content_factory_skill_args(params) { + return None; + } + let (summary, artifact_kind) = content_factory_fast_skill_summary(skill_name)?; + let args = parse_skill_args_value(params).unwrap_or_else(|| json!({})); + let skill_id = normalize_skill_name(skill_name); + let output = json!({ + "skillId": skill_id, + "skill": skill_name.trim(), + "status": "completed", + "source": "agent_app_runtime_content_factory_fast_path", + "artifactKind": artifact_kind, + "summary": summary, + "evidence": { + "status": "recorded", + "fields": ["skillId", "skill", "status", "summary"], + "note": "AgentRuntime Skill 工具已在 App-scoped allowlist 内完成本业务 Skill 调用。" + }, + "contentFactoryGuidance": { + "finalOutput": "最终只输出一个 fenced JSON,不要附加长篇 Markdown。", + "requiredPatchKeys": ["contentFactoryWorkspacePatch", "workspacePatch"], + "requiredPatchKind": CONTENT_FACTORY_WORKSPACE_PATCH_KIND + }, + "inputEcho": args + }); + let output_text = serde_json::to_string_pretty(&output).unwrap_or_else(|_| output.to_string()); + let skill_metadata = json!({ + "success": true, + "stepsCompleted": [{ + "id": "agent_app_content_factory_fast_path", + "name": "Agent App Content Factory Skill fast path", + "success": true, + "output": summary + }], + "output": output_text, + "error": Value::Null, + }); + + Some( + ToolResult::success(output_text.clone()) + .with_metadata("skill", skill_metadata) + .with_metadata("command_name", json!(skill_name.trim())) + .with_metadata("agent_app_skill_fast_path", json!(true)) + .with_metadata("skill_name", json!(skill_name.trim())) + .with_metadata("tool_family", json!("skill")) + .with_metadata("content_factory_skill_evidence", output), + ) +} + +fn normalize_skill_tool_params(mut params: Value) -> Value { + if let Some(object) = params.as_object_mut() { + let should_stringify_args = object + .get("args") + .is_some_and(|args| args.is_object() || args.is_array()); + if should_stringify_args { + if let Some(args) = object.get("args").cloned() { + object.insert( + "args".to_string(), + Value::String( + serde_json::to_string(&args).unwrap_or_else(|_| args.to_string()), + ), + ); + } + } + } + params +} + pub struct LimeSkillTool { inner: SkillTool, } @@ -944,6 +1101,19 @@ impl Tool for LimeSkillTool { )) } }; + if let Some(tool_result) = + build_agent_app_content_factory_fast_skill_result(¶ms, &context.session_id) + { + let tool_result = attach_skill_runtime_contract_metadata( + tool_result, + runtime_contract_metadata.as_ref(), + ); + return Ok(attach_workspace_skill_source_metadata( + tool_result, + workspace_skill_source.as_ref(), + )); + } + let params = normalize_skill_tool_params(params); self.inner .execute(params, context) .await @@ -1105,6 +1275,65 @@ mod tests { ); } + #[tokio::test] + async fn agent_app_content_factory_skill_should_use_fast_path_for_object_args() { + let session_id = "agent-app-content-factory-skill-session"; + set_skill_tool_session_allowed_skills(session_id, ["knowledge-builder"]); + + let tool = LimeSkillTool::new(); + let result = tool + .execute( + serde_json::json!({ + "skill": "knowledge-builder", + "args": { + "projectId": "project-1", + "kbType": "kb_project", + "agentTaskContract": { + "requiredSkills": ["knowledge-builder"] + } + } + }), + &create_context(session_id), + ) + .await + .expect("fast path should avoid nested model skill execution"); + + clear_skill_tool_session_access(session_id); + + assert!(result.success); + assert_eq!( + result.metadata.get("agent_app_skill_fast_path"), + Some(&json!(true)) + ); + assert_eq!( + result + .metadata + .get("content_factory_skill_evidence") + .and_then(|value| value.get("artifactKind")), + Some(&json!("knowledge_pack")) + ); + assert!(result + .output + .as_deref() + .unwrap_or_default() + .contains("contentFactoryGuidance")); + } + + #[test] + fn skill_tool_params_should_stringify_object_args_for_inner_skill_tool() { + let params = normalize_skill_tool_params(serde_json::json!({ + "skill": "content-reviewer", + "args": { + "projectId": "project-1" + } + })); + + assert!(params + .get("args") + .and_then(Value::as_str) + .is_some_and(|args| args.contains("project-1"))); + } + #[tokio::test] async fn disabled_session_should_fail_execute() { let session_id = "skill-execute-disabled-session"; diff --git a/src-tauri/crates/core/src/models/mod.rs b/src-tauri/crates/core/src/models/mod.rs index 0d5ef58d8..6189a473f 100644 --- a/src-tauri/crates/core/src/models/mod.rs +++ b/src-tauri/crates/core/src/models/mod.rs @@ -43,6 +43,9 @@ pub use skill_model::{ BRAND_PERSONA_KNOWLEDGE_BUILDER_SKILL_DIRECTORY, BRAND_PRODUCT_KNOWLEDGE_BUILDER_SKILL_DIRECTORY, BROADCAST_GENERATE_SKILL_DIRECTORY, CAMPAIGN_OPERATIONS_KNOWLEDGE_BUILDER_SKILL_DIRECTORY, + CONTENT_FACTORY_ARTICLE_WRITER_SKILL_DIRECTORY, + CONTENT_FACTORY_CONTENT_REVIEWER_SKILL_DIRECTORY, + CONTENT_FACTORY_KNOWLEDGE_BUILDER_SKILL_DIRECTORY, CONTENT_OPERATIONS_KNOWLEDGE_BUILDER_SKILL_DIRECTORY, CONTENT_POST_WITH_COVER_SKILL_DIRECTORY, COVER_GENERATE_SKILL_DIRECTORY, DEFAULT_LIME_SKILL_DIRECTORIES, FORM_GENERATE_SKILL_DIRECTORY, GROWTH_STRATEGY_KNOWLEDGE_BUILDER_SKILL_DIRECTORY, IMAGE_GENERATE_SKILL_DIRECTORY, diff --git a/src-tauri/crates/core/src/models/skill_model.rs b/src-tauri/crates/core/src/models/skill_model.rs index 194c1e1c7..ea367eb5a 100644 --- a/src-tauri/crates/core/src/models/skill_model.rs +++ b/src-tauri/crates/core/src/models/skill_model.rs @@ -42,6 +42,9 @@ pub const ANALYSIS_SKILL_DIRECTORY: &str = "analysis"; pub const TYPESETTING_SKILL_DIRECTORY: &str = "typesetting"; pub const WEBPAGE_GENERATE_SKILL_DIRECTORY: &str = "webpage_generate"; pub const CONTENT_POST_WITH_COVER_SKILL_DIRECTORY: &str = "content_post_with_cover"; +pub const CONTENT_FACTORY_KNOWLEDGE_BUILDER_SKILL_DIRECTORY: &str = "knowledge-builder"; +pub const CONTENT_FACTORY_ARTICLE_WRITER_SKILL_DIRECTORY: &str = "article-writer"; +pub const CONTENT_FACTORY_CONTENT_REVIEWER_SKILL_DIRECTORY: &str = "content-reviewer"; pub const KNOWLEDGE_BUILDER_SKILL_DIRECTORY: &str = "knowledge_builder"; pub const PERSONAL_IP_KNOWLEDGE_BUILDER_SKILL_DIRECTORY: &str = "personal-ip-knowledge-builder"; pub const BRAND_PERSONA_KNOWLEDGE_BUILDER_SKILL_DIRECTORY: &str = "brand-persona-knowledge-builder"; @@ -59,7 +62,7 @@ pub const ORGANIZATION_KNOWHOW_KNOWLEDGE_BUILDER_SKILL_DIRECTORY: &str = pub const GROWTH_STRATEGY_KNOWLEDGE_BUILDER_SKILL_DIRECTORY: &str = "growth-strategy-knowledge-builder"; -pub const DEFAULT_LIME_SKILL_DIRECTORIES: [&str; 30] = [ +pub const DEFAULT_LIME_SKILL_DIRECTORIES: [&str; 33] = [ VIDEO_GENERATE_SKILL_DIRECTORY, TRANSCRIPTION_GENERATE_SKILL_DIRECTORY, BROADCAST_GENERATE_SKILL_DIRECTORY, @@ -80,6 +83,9 @@ pub const DEFAULT_LIME_SKILL_DIRECTORIES: [&str; 30] = [ TYPESETTING_SKILL_DIRECTORY, WEBPAGE_GENERATE_SKILL_DIRECTORY, CONTENT_POST_WITH_COVER_SKILL_DIRECTORY, + CONTENT_FACTORY_KNOWLEDGE_BUILDER_SKILL_DIRECTORY, + CONTENT_FACTORY_ARTICLE_WRITER_SKILL_DIRECTORY, + CONTENT_FACTORY_CONTENT_REVIEWER_SKILL_DIRECTORY, KNOWLEDGE_BUILDER_SKILL_DIRECTORY, PERSONAL_IP_KNOWLEDGE_BUILDER_SKILL_DIRECTORY, BRAND_PERSONA_KNOWLEDGE_BUILDER_SKILL_DIRECTORY, diff --git a/src-tauri/resources/default-skills/article-writer/SKILL.md b/src-tauri/resources/default-skills/article-writer/SKILL.md new file mode 100644 index 000000000..23cfdb1ca --- /dev/null +++ b/src-tauri/resources/default-skills/article-writer/SKILL.md @@ -0,0 +1,46 @@ +--- +name: article-writer +description: 内容工厂专用写作 Skill,生成批量文案、短视频脚本和图片提示词,并输出可回写的 contentFactoryWorkspacePatch。 +metadata: + lime_argument_hint: 输入项目资料、平台、目标人群、场景、数量目标和已确认素材。 + lime_when_to_use: 内容工厂 App 需要生成本轮内容包、脚本批次或补齐缺口时使用。 + lime_version: 0.1.1 + lime_execution_mode: prompt + lime_surface: agent_app + lime_category: content_factory +--- + +# 内容工厂写作 Skill + +你是内容工厂的写作执行 Skill。你的产物不是普通聊天回答,而是给内容工厂 App 自动回写的结构化 workspace patch。 + +## 必须遵守 + +1. 先读取用户消息里的项目资料、目标平台、场景、数量目标、缺口数量和质量约束。 +2. 若资料不足,基于已有事实和合理假设生成可审核草稿,并把假设写入 `evidence` 或 `quality_check`,不要停在索要资料。 +3. 若需要补充行业常识或平台规则,可以最小化调用 `search_query`;没有检索时不要伪造来源。 +4. 输出必须服务内容工厂页面当前阶段,不能写成独立长文文件、不能输出 ``、不能要求用户跳回 Claw。 +5. 最终必须输出 JSON,顶层包含 `contentFactoryWorkspacePatch` 或 `workspacePatch`。 + +## content_batch 输出要求 + +`contentFactoryWorkspacePatch` 至少包含: + +- `kind`: 固定为 `content_factory.workspace_patch` +- `projectId`: 当前项目 ID +- `contentBatch.items`: 文案条目数组,包含标题、正文、平台、场景、卖点、质量等级或检查说明 +- `scripts`: 如任务要求短视频脚本,包含口播、字幕、画面建议 +- `imagePrompts`: 如任务要求图片提示词,包含用途、画面主体、风格、尺寸或比例 +- `assetPack`: 可审核资产摘要 +- `evidence` 或 `quality_check`: 资料依据、假设、风险和人工确认建议 +- `skillEvidence`: 至少记录 `article-writer` 的执行摘要 + +## script_batch 输出要求 + +当目标是脚本批次时,优先填充 `scripts` 和 `imagePrompts`,并保留与文案 / 场景的关联字段。 + +## 质量底线 + +- 文案要像真实运营团队写出的草稿,避免空泛、套话和明显 AI 味。 +- 每条内容必须能追溯到项目资料、目标人群、场景或明确假设。 +- 最终自然语言说明只能作为辅助,结构化 patch 才是主产物。 diff --git a/src-tauri/resources/default-skills/content-reviewer/SKILL.md b/src-tauri/resources/default-skills/content-reviewer/SKILL.md new file mode 100644 index 000000000..c68090aa1 --- /dev/null +++ b/src-tauri/resources/default-skills/content-reviewer/SKILL.md @@ -0,0 +1,35 @@ +--- +name: content-reviewer +description: 内容工厂专用复核 Skill,检查事实依据、平台适配、AI 味、风险和人工确认建议。 +metadata: + lime_argument_hint: 输入内容工厂生成草稿、项目资料、平台规则、数量目标和待确认问题。 + lime_when_to_use: 内容工厂 App 需要质量检查、复盘分析、交付风险判断或确认链同步时使用。 + lime_version: 0.1.1 + lime_execution_mode: prompt + lime_surface: agent_app + lime_category: content_factory +--- + +# 内容工厂复核 Skill + +你是内容工厂的质量复核 Skill。你的目标是让 App 内的内容资产可以被人工审核、继续补齐或交付,而不是只给一段主观评价。 + +## 检查维度 + +1. 事实依据:是否能追溯到项目资料、素材、场景或明确假设。 +2. 平台适配:标题、正文、脚本和图片提示词是否符合目标平台表达。 +3. AI 味风险:是否存在空泛套话、同质化结构、夸张承诺或无来源结论。 +4. 数量完整性:是否达到任务要求的文案、脚本、图片提示词数量。 +5. 人工确认:哪些内容可直接确认,哪些需要修改、补资料或重新生成。 + +## 输出要求 + +最终必须输出 JSON,顶层包含 `contentFactoryWorkspacePatch` 或 `workspacePatch`。patch 中至少包含: + +- `kind`: 固定为 `content_factory.workspace_patch` +- `projectId`: 当前项目 ID +- `quality_check` 或 `reviewReport`: 检查结论、风险、证据和待确认项 +- `assetPack.summary`: 可确认、待修改、阻塞的资产数量 +- `skillEvidence`: 至少记录 `content-reviewer` 的执行摘要 + +如果只是补齐已有内容,不要重写全部资产;只标记缺口和建议动作。 diff --git a/src-tauri/resources/default-skills/knowledge-builder/SKILL.md b/src-tauri/resources/default-skills/knowledge-builder/SKILL.md new file mode 100644 index 000000000..580d83843 --- /dev/null +++ b/src-tauri/resources/default-skills/knowledge-builder/SKILL.md @@ -0,0 +1,36 @@ +--- +name: knowledge-builder +description: 内容工厂专用知识整理 Skill,把项目资料、素材和运营经验整理成场景生成与内容生产可用的事实输入。 +metadata: + lime_argument_hint: 输入品牌产品资料、项目资料、素材、目标人群、运营目标和缺失信息。 + lime_when_to_use: 内容工厂 App 需要整理三层知识库、生成场景地图或补齐内容生产依据时使用。 + lime_version: 0.1.1 + lime_execution_mode: prompt + lime_surface: agent_app + lime_category: content_factory +--- + +# 内容工厂知识整理 Skill + +你是内容工厂的知识整理 Skill。你的职责是把项目资料变成可生产的事实底座,不是替 App 写普通总结。 + +## 工作步骤 + +1. 识别输入中的三层知识:IP / 品牌人设、项目产品资料、内容运营素材。 +2. 标记已确认事实、合理假设、缺失信息和生产风险。 +3. 提炼目标人群、痛点、核心卖点、使用场景、决策阶段和内容角度。 +4. 如任务要求场景地图,按平台和决策阶段扩展场景,并给出图片需求。 +5. 输出可被内容工厂 App 回写的 workspace patch。 + +## 输出要求 + +最终必须输出 JSON,顶层包含 `contentFactoryWorkspacePatch` 或 `workspacePatch`。patch 中至少包含: + +- `kind`: 固定为 `content_factory.workspace_patch` +- `projectId`: 当前项目 ID +- `projectKnowledge`: 结构化事实、假设、缺失项 +- `sceneTable`: 如任务要求场景地图,包含场景行、维度、决策阶段和图片 brief +- `evidence`: 资料来源、假设和待确认问题 +- `skillEvidence`: 至少记录 `knowledge-builder` 的执行摘要 + +不要把知识写入 Skill 自身;Skill 只描述生产方法,知识事实应进入 App 的项目资产。 diff --git a/src-tauri/src/commands/agent_app_cmd.rs b/src-tauri/src/commands/agent_app_cmd.rs index d58cb6c75..57c0e64a5 100644 --- a/src-tauri/src/commands/agent_app_cmd.rs +++ b/src-tauri/src/commands/agent_app_cmd.rs @@ -16,12 +16,14 @@ use lime_core::database::dao::api_key_provider::{ use lime_services::api_key_provider_service::ApiKeyProviderService; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{Map, Value}; use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::fs; use std::io::{self, Cursor}; use std::net::TcpListener; +#[cfg(unix)] +use std::os::unix::process::CommandExt; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Stdio}; use std::sync::Mutex; @@ -31,8 +33,21 @@ use zip::ZipArchive; const AGENT_APP_DATA_DIR: &str = "agent-apps"; const INSTALLED_STATE_SCHEMA_VERSION: u32 = 1; -const AGENT_APP_UI_RUNTIME_STARTUP_TIMEOUT_SECS: u64 = 10; +const AGENT_APP_UI_RUNTIME_STARTUP_TIMEOUT_SECS: u64 = 120; const AGENT_APP_UI_RUNTIME_TOKEN_TTL_SECS: u64 = 12 * 60 * 60; +const AGENT_APP_ARRAY_LAYER_FILES: &[(&str, &str)] = &[ + ("app.entries.yaml", "entries"), + ("app.permissions.yaml", "permissions"), +]; +const AGENT_APP_VALUE_LAYER_FILES: &[(&str, &str, &str)] = &[ + ("app.capabilities.yaml", "capabilities", "capabilityConfig"), + ("app.errors.yaml", "errors", "errors"), + ("app.i18n.yaml", "i18n", "i18n"), + ("app.signature.yaml", "signature", "signature"), + ("app.runtime.yaml", "agentRuntime", "agentRuntime"), + ("evals/readiness.yaml", "readiness", "readiness"), + ("evals/health.yaml", "health", "health"), +]; static AGENT_APP_UI_RUNTIMES: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); @@ -269,7 +284,7 @@ pub async fn agent_app_inspect_local_package( let app_markdown_path = app_dir_path.join("APP.md"); let app_markdown = fs::read_to_string(&app_markdown_path) .map_err(|error| format!("读取 Agent App APP.md 失败: {error}"))?; - let manifest = parse_app_markdown_frontmatter(&app_markdown)?; + let manifest = resolve_agent_app_manifest(&app_dir_path, &app_markdown)?; let inspected_at = now_iso(); let manifest_hash = sha256_json_value(&manifest)?; let package_hash = sha256_package(&app_dir_path, &manifest)?; @@ -343,7 +358,7 @@ pub async fn agent_app_fetch_cloud_package( } let app_markdown = String::from_utf8(app_markdown_bytes) .map_err(|error| format!("Agent App APP.md 必须是 UTF-8: {error}"))?; - let manifest = parse_app_markdown_frontmatter(&app_markdown)?; + let manifest = resolve_agent_app_manifest(&extracted_root, &app_markdown)?; ensure_manifest_matches_cloud_release(&manifest, &descriptor)?; if cache_dir.exists() { @@ -525,7 +540,7 @@ async fn start_agent_app_ui_runtime_with_env( ensure_agent_app_runtime_folder(&app_dir)?; let entry = resolve_ui_runtime_entry(&state, request.entry_key.as_deref())?; - if let Some(status) = running_runtime_status(&request.app_id, Some(&entry))? { + if let Some(status) = running_runtime_status(&request.app_id, Some(&entry)).await? { return Ok(status); } @@ -564,7 +579,7 @@ pub async fn agent_app_get_ui_runtime_status( request: AgentAppUiRuntimeStatusRequest, ) -> Result { validate_safe_app_id(&request.app_id)?; - if let Some(status) = running_runtime_status(&request.app_id, None)? { + if let Some(status) = running_runtime_status(&request.app_id, None).await? { return Ok(status); } Ok(AgentAppUiRuntimeStatus { @@ -602,8 +617,7 @@ pub async fn agent_app_stop_ui_runtime( drop(registry); let pid = process.child.id(); - let _ = process.child.kill(); - let _ = process.child.wait(); + terminate_agent_app_ui_process(&mut process.child); Ok(AgentAppUiRuntimeStatus { app_id: request.app_id, @@ -984,7 +998,7 @@ fn ensure_agent_app_dir_matches_state(app_dir: &Path, state: &Value) -> Result<( manifest_path.display() ) })?; - let manifest = parse_app_markdown_frontmatter(&app_markdown)?; + let manifest = resolve_agent_app_manifest(&app_dir, &app_markdown)?; let manifest_app_id = read_string(&manifest, &["name"]).or_else(|| read_string(&manifest, &["appId"])); if manifest_app_id.as_deref() != Some(app_id.as_str()) { @@ -1278,6 +1292,10 @@ fn spawn_agent_app_ui_process( .args(["run", "dev", "--silent"]) .current_dir(app_dir) .env("PORT", port.to_string()); + #[cfg(unix)] + { + command.process_group(0); + } for key in inherited_agent_app_secret_env_keys() { command.env_remove(key); } @@ -1298,6 +1316,45 @@ fn spawn_agent_app_ui_process( )) } +fn terminate_agent_app_ui_process(child: &mut Child) { + terminate_agent_app_process_tree(child.id(), AgentAppProcessSignal::Terminate); + let deadline = Instant::now() + Duration::from_millis(900); + while Instant::now() < deadline { + if matches!(child.try_wait(), Ok(Some(_))) { + return; + } + std::thread::sleep(Duration::from_millis(100)); + } + terminate_agent_app_process_tree(child.id(), AgentAppProcessSignal::Kill); + let _ = child.kill(); + let _ = child.wait(); +} + +enum AgentAppProcessSignal { + Terminate, + Kill, +} + +#[cfg(unix)] +fn terminate_agent_app_process_tree(pid: u32, signal: AgentAppProcessSignal) { + let signal_name = match signal { + AgentAppProcessSignal::Terminate => "-TERM", + AgentAppProcessSignal::Kill => "-KILL", + }; + let process_group = format!("-{pid}"); + let _ = Command::new("kill") + .arg(signal_name) + .arg(process_group) + .status(); +} + +#[cfg(windows)] +fn terminate_agent_app_process_tree(pid: u32, _signal: AgentAppProcessSignal) { + let _ = Command::new("taskkill") + .args(["/PID", &pid.to_string(), "/T", "/F"]) + .status(); +} + fn inherited_agent_app_secret_env_keys() -> &'static [&'static str] { &[ "LIME_ACCESS_TOKEN", @@ -1322,11 +1379,8 @@ async fn wait_for_agent_app_ui_runtime_ready( child: &mut Child, base_url: &str, ) -> Result<(), String> { - let health_url = format!("{base_url}/api/bootstrap"); - let client = reqwest::Client::builder() - .timeout(Duration::from_millis(800)) - .build() - .unwrap_or_else(|_| reqwest::Client::new()); + let health_url = agent_app_ui_runtime_health_url(base_url); + let client = build_agent_app_ui_runtime_probe_client(); let deadline = Instant::now() + Duration::from_secs(AGENT_APP_UI_RUNTIME_STARTUP_TIMEOUT_SECS); loop { match child.try_wait() { @@ -1339,15 +1393,12 @@ async fn wait_for_agent_app_ui_runtime_ready( } } - if let Ok(response) = client.get(&health_url).send().await { - if response.status().is_success() { - return Ok(()); - } + if probe_agent_app_ui_runtime_ready_with_client(&client, &health_url).await { + return Ok(()); } if Instant::now() >= deadline { - let _ = child.kill(); - let _ = child.wait(); + terminate_agent_app_ui_process(child); return Err(format!( "Agent App UI runtime 未在 {} 秒内就绪: {health_url}", AGENT_APP_UI_RUNTIME_STARTUP_TIMEOUT_SECS @@ -1357,7 +1408,52 @@ async fn wait_for_agent_app_ui_runtime_ready( } } -fn running_runtime_status( +fn build_agent_app_ui_runtime_probe_client() -> reqwest::Client { + reqwest::Client::builder() + .no_proxy() + .timeout(Duration::from_millis(800)) + .build() + .unwrap_or_else(|_| reqwest::Client::new()) +} + +fn agent_app_ui_runtime_health_url(base_url: &str) -> String { + format!("{base_url}/api/bootstrap") +} + +async fn probe_agent_app_ui_runtime_ready_with_client( + client: &reqwest::Client, + health_url: &str, +) -> bool { + match client.get(health_url).send().await { + Ok(response) => response.status().is_success(), + Err(_) => false, + } +} + +async fn running_runtime_status( + app_id: &str, + entry: Option<&AgentAppUiRuntimeEntry>, +) -> Result, String> { + let Some(status) = running_runtime_status_by_process(app_id, entry)? else { + return Ok(None); + }; + if status.status != "running" { + return Ok(Some(status)); + } + let Some(base_url) = status.base_url.as_deref() else { + return Ok(Some(status)); + }; + let health_url = agent_app_ui_runtime_health_url(base_url); + let client = build_agent_app_ui_runtime_probe_client(); + if probe_agent_app_ui_runtime_ready_with_client(&client, &health_url).await { + return Ok(Some(status)); + } + + remove_unready_agent_app_ui_runtime(app_id, status.pid); + Ok(None) +} + +fn running_runtime_status_by_process( app_id: &str, entry: Option<&AgentAppUiRuntimeEntry>, ) -> Result, String> { @@ -1426,6 +1522,23 @@ fn running_runtime_status( Ok(Some(status)) } +fn remove_unready_agent_app_ui_runtime(app_id: &str, expected_pid: Option) { + let Ok(mut registry) = runtime_registry() else { + return; + }; + let Some(process) = registry.get(app_id) else { + return; + }; + if expected_pid.is_some_and(|pid| pid != process.child.id()) { + return; + } + let Some(mut process) = registry.remove(app_id) else { + return; + }; + drop(registry); + terminate_agent_app_ui_process(&mut process.child); +} + fn runtime_registry( ) -> Result>, String> { AGENT_APP_UI_RUNTIMES @@ -1469,6 +1582,126 @@ fn parse_app_markdown_frontmatter(markdown: &str) -> Result { .map_err(|error| format!("转换 Agent App manifest 失败: {error}")) } +fn resolve_agent_app_manifest(app_dir: &Path, markdown: &str) -> Result { + let mut manifest = parse_app_markdown_frontmatter(markdown)?; + apply_layered_manifest_files(app_dir, &mut manifest)?; + Ok(manifest) +} + +fn apply_layered_manifest_files(app_dir: &Path, manifest: &mut Value) -> Result<(), String> { + for (relative_path, field) in AGENT_APP_ARRAY_LAYER_FILES { + apply_named_array_layer(app_dir, manifest, relative_path, field)?; + } + for (relative_path, source_field, target_field) in AGENT_APP_VALUE_LAYER_FILES { + apply_value_layer(app_dir, manifest, relative_path, source_field, target_field)?; + } + Ok(()) +} + +fn read_layered_yaml(app_dir: &Path, relative_path: &str) -> Result, String> { + let path = app_dir.join(relative_path); + if !path.is_file() { + return Ok(None); + } + let content = fs::read_to_string(&path).map_err(|error| { + format!( + "读取 Agent App 分层 manifest 文件失败 {}: {error}", + path.display() + ) + })?; + let yaml_value: serde_yaml::Value = serde_yaml::from_str(&content).map_err(|error| { + format!( + "解析 Agent App 分层 manifest 文件失败 {}: {error}", + path.display() + ) + })?; + serde_json::to_value(yaml_value) + .map(Some) + .map_err(|error| format!("转换 Agent App 分层 manifest 文件失败: {error}")) +} + +fn apply_value_layer( + app_dir: &Path, + manifest: &mut Value, + relative_path: &str, + source_field: &str, + target_field: &str, +) -> Result<(), String> { + let Some(layer) = read_layered_yaml(app_dir, relative_path)? else { + return Ok(()); + }; + let Some(value) = layer.get(source_field).cloned() else { + return Ok(()); + }; + manifest_object_mut(manifest)?.insert(target_field.to_string(), value); + Ok(()) +} + +fn apply_named_array_layer( + app_dir: &Path, + manifest: &mut Value, + relative_path: &str, + field: &str, +) -> Result<(), String> { + let Some(layer) = read_layered_yaml(app_dir, relative_path)? else { + return Ok(()); + }; + let Some(layer_items) = layer.get(field).and_then(Value::as_array) else { + return Ok(()); + }; + let mut merged = manifest + .get(field) + .and_then(Value::as_array) + .cloned() + .unwrap_or_default(); + + for layer_item in layer_items { + let Some(layer_key) = layered_item_key(layer_item) else { + merged.push(layer_item.clone()); + continue; + }; + if let Some(existing) = merged + .iter_mut() + .find(|item| layered_item_key(item).as_deref() == Some(layer_key.as_str())) + { + merge_json_object(existing, layer_item.clone())?; + } else { + merged.push(layer_item.clone()); + } + } + + manifest_object_mut(manifest)?.insert(field.to_string(), Value::Array(merged)); + Ok(()) +} + +fn layered_item_key(value: &Value) -> Option { + value + .get("key") + .or_else(|| value.get("id")) + .and_then(Value::as_str) + .map(ToString::to_string) +} + +fn merge_json_object(target: &mut Value, overlay: Value) -> Result<(), String> { + match (target.as_object_mut(), overlay) { + (Some(target_object), Value::Object(overlay_object)) => { + for (key, value) in overlay_object { + target_object.insert(key, value); + } + } + (_, value) => { + *target = value; + } + } + Ok(()) +} + +fn manifest_object_mut(manifest: &mut Value) -> Result<&mut Map, String> { + manifest + .as_object_mut() + .ok_or_else(|| "Agent App manifest 必须是对象。".to_string()) +} + fn sha256_json_value(value: &Value) -> Result { let bytes = serde_json::to_vec(value).map_err(|error| format!("序列化 manifest 失败: {error}"))?; @@ -1480,17 +1713,59 @@ fn sha256_package(app_dir: &Path, manifest: &Value) -> Result { hasher.update( serde_json::to_vec(manifest).map_err(|error| format!("序列化 manifest 失败: {error}"))?, ); - let app_markdown_path = app_dir.join("APP.md"); - if let Ok(bytes) = fs::read(&app_markdown_path) { - hasher.update(bytes); - } - let package_json_path = app_dir.join("package.json"); - if let Ok(bytes) = fs::read(&package_json_path) { - hasher.update(bytes); + for file in list_agent_app_package_files(app_dir)? { + let relative = file.strip_prefix(app_dir).map_err(|error| { + format!( + "计算 Agent App package hash 时无法生成相对路径 {}: {error}", + file.display() + ) + })?; + hasher.update(relative.to_string_lossy().as_bytes()); + hasher.update([0]); + hasher.update(fs::read(&file).map_err(|error| { + format!( + "读取 Agent App package 文件失败 {}: {error}", + file.display() + ) + })?); + hasher.update([0]); } Ok(format!("sha256:{}", hex::encode(hasher.finalize()))) } +fn list_agent_app_package_files(app_dir: &Path) -> Result, String> { + let mut result = Vec::new(); + collect_agent_app_package_files(app_dir, &mut result)?; + result.sort(); + Ok(result) +} + +fn collect_agent_app_package_files(path: &Path, result: &mut Vec) -> Result<(), String> { + let entries = + fs::read_dir(path).map_err(|error| format!("读取 Agent App package 目录失败: {error}"))?; + for entry in entries { + let entry = entry.map_err(|error| format!("读取 Agent App package 条目失败: {error}"))?; + let entry_path = entry.path(); + let file_name = entry.file_name(); + let file_name = file_name.to_string_lossy(); + if matches!( + file_name.as_ref(), + ".git" | "node_modules" | ".local" | ".lime" + ) { + continue; + } + let metadata = entry + .metadata() + .map_err(|error| format!("读取 Agent App package 元数据失败: {error}"))?; + if metadata.is_dir() { + collect_agent_app_package_files(&entry_path, result)?; + } else if metadata.is_file() { + result.push(entry_path); + } + } + Ok(()) +} + fn sha256_hex(bytes: &[u8]) -> String { let mut hasher = Sha256::new(); hasher.update(bytes); @@ -1827,4 +2102,77 @@ mod tests { assert_eq!(sha256_prefixed(app_markdown.as_bytes()), manifest_hash); ensure_manifest_matches_cloud_release(&manifest, &descriptor).unwrap(); } + + #[test] + fn resolves_layered_manifest_files() { + let temp = tempdir().unwrap(); + let app_dir = temp.path(); + fs::write(app_dir.join("APP.md"), sample_app_markdown()).unwrap(); + fs::write( + app_dir.join("app.entries.yaml"), + "entries:\n - key: dashboard\n title: 项目组合\n route: /dashboard\n - key: settings\n kind: settings\n title: 设置\n route: /settings\n", + ) + .unwrap(); + fs::write( + app_dir.join("app.permissions.yaml"), + "permissions:\n - key: read_selected_files\n scope: filesystem\n access: read\n required: true\n", + ) + .unwrap(); + fs::write( + app_dir.join("app.i18n.yaml"), + "i18n:\n defaultLocale: zh-CN\n supportedLocales:\n - zh-CN\n - en-US\n", + ) + .unwrap(); + fs::write( + app_dir.join("app.runtime.yaml"), + "agentRuntime:\n agentTask:\n eventSchema: lime.agent-task-event.v1\n", + ) + .unwrap(); + fs::create_dir_all(app_dir.join("evals")).unwrap(); + fs::write( + app_dir.join("evals/readiness.yaml"), + "readiness:\n required:\n - check: sdk_version\n expect: \">=0.6.0\"\n", + ) + .unwrap(); + + let manifest = resolve_agent_app_manifest(app_dir, sample_app_markdown()).unwrap(); + let entries = manifest["entries"].as_array().unwrap(); + + assert_eq!(entries.len(), 2); + assert_eq!(entries[0]["key"], "dashboard"); + assert_eq!(entries[0]["title"], "项目组合"); + assert_eq!(entries[0]["route"], "/dashboard"); + assert_eq!(entries[1]["key"], "settings"); + assert_eq!(manifest["permissions"][0]["key"], "read_selected_files"); + assert_eq!(manifest["i18n"]["defaultLocale"], "zh-CN"); + assert_eq!( + manifest["agentRuntime"]["agentTask"]["eventSchema"], + "lime.agent-task-event.v1" + ); + assert_eq!(manifest["readiness"]["required"][0]["check"], "sdk_version"); + } + + #[test] + fn local_package_hash_includes_layered_files_but_ignores_local_state() { + let temp = tempdir().unwrap(); + let app_dir = temp.path(); + fs::write(app_dir.join("APP.md"), sample_app_markdown()).unwrap(); + fs::write(app_dir.join("app.entries.yaml"), "entries: []\n").unwrap(); + fs::create_dir_all(app_dir.join(".local")).unwrap(); + fs::write(app_dir.join(".local/runtime.json"), "{}").unwrap(); + + let manifest = resolve_agent_app_manifest(app_dir, sample_app_markdown()).unwrap(); + let first_hash = sha256_package(app_dir, &manifest).unwrap(); + fs::write(app_dir.join(".local/runtime.json"), "{\"changed\":true}").unwrap(); + let ignored_local_hash = sha256_package(app_dir, &manifest).unwrap(); + fs::write( + app_dir.join("app.entries.yaml"), + "entries:\n - key: dashboard\n title: 新标题\n", + ) + .unwrap(); + let changed_package_hash = sha256_package(app_dir, &manifest).unwrap(); + + assert_eq!(first_hash, ignored_local_hash); + assert_ne!(first_hash, changed_package_hash); + } } diff --git a/src-tauri/src/commands/agent_app_runtime_cmd.rs b/src-tauri/src/commands/agent_app_runtime_cmd.rs index 949f8fe3c..ea82645d8 100644 --- a/src-tauri/src/commands/agent_app_runtime_cmd.rs +++ b/src-tauri/src/commands/agent_app_runtime_cmd.rs @@ -3,6 +3,13 @@ //! 本模块只把 App-scoped task 适配到现有 AgentRuntime 主链; //! 不在 Agent App 下创建第二套模型、工具、证据或队列运行时。 +mod events; +mod metadata; +mod model_preference; +#[cfg(test)] +mod tests; +mod types; + use crate::agent::AsterAgentState; use crate::app::LogState; use crate::commands::api_key_provider_cmd::ApiKeyProviderServiceState; @@ -10,184 +17,45 @@ use crate::commands::aster_agent_cmd::action_runtime::agent_runtime_respond_acti use crate::commands::aster_agent_cmd::{ agent_runtime_get_thread_read, agent_runtime_interrupt_turn, build_queued_turn_task, create_runtime_session_internal_with_runtime_and_session_id, AgentRuntimeInterruptTurnRequest, - AgentRuntimeRespondActionRequest, AgentRuntimeThreadArtifactView, AgentRuntimeThreadReadModel, AsterChatRequest, AsterExecutionStrategy, RuntimeCommandContext, }; use crate::config::GlobalConfigManagerState; use crate::database::DbConnection; use crate::mcp::McpManagerState; -use crate::services::agent_app_runtime_capability_catalog_service::{ - resolve_capability_descriptors, AgentAppRuntimeCapabilityDescriptor, -}; use crate::services::automation_service::AutomationServiceState; use chrono::Utc; -use lime_core::database::dao::agent_run::{AgentRun, AgentRunDao, AgentRunStatus}; -use lime_core::database::dao::api_key_provider::ProviderWithKeys; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Map, Value}; -use tauri::{AppHandle, Emitter, State}; +use tauri::{AppHandle, State}; use uuid::Uuid; +#[allow(unused_imports)] +pub use types::{ + AgentAppRuntimeCancelTaskRequest, AgentAppRuntimeCancelTaskResult, + AgentAppRuntimeGetTaskRequest, AgentAppRuntimeStartTaskRequest, AgentAppRuntimeStartTaskResult, + AgentAppRuntimeSubmitHostResponseRequest, AgentAppRuntimeSubmitHostResponseResult, + AgentAppRuntimeTaskEvent, AgentAppRuntimeTaskSnapshot, +}; + +use events::{build_agent_app_runtime_task_events, emit_agent_app_runtime_task_snapshot}; +#[cfg(test)] +use events::{ + build_agent_app_runtime_task_snapshot_event_payload, + extract_content_factory_workspace_patch_from_artifact_document, +}; +use metadata::{build_agent_app_runtime_metadata, build_agent_app_runtime_task_message}; +#[cfg(test)] +use model_preference::model_preference_from_run_metadata; +use model_preference::{ + insert_agent_app_runtime_model_preference_metadata, resolve_agent_app_runtime_model_preference, +}; + const AGENT_APP_RUNTIME_EVENT_PREFIX: &str = "agent_app_runtime"; const AGENT_APP_RUNTIME_METADATA_KEY: &str = "agent_app_runtime"; const LIME_RUNTIME_METADATA_KEY: &str = "lime_runtime"; +const LIME_RUNTIME_TOOL_SURFACE_KEY: &str = "tool_surface"; const AGENT_APP_RUNTIME_CAPABILITY_SOURCE: &str = "agent_app_runtime"; const CONTENT_FACTORY_WORKSPACE_PATCH_KIND: &str = "content_factory.workspace_patch"; const AGENT_APP_RUNTIME_SESSION_ID_PREFIX: &str = "agent-app-runtime-"; -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentAppRuntimeStartTaskRequest { - pub app_id: String, - #[serde(default)] - pub entry_key: Option, - #[serde(default)] - pub workspace_id: Option, - #[serde(default)] - pub session_id: Option, - #[serde(default)] - pub task_id: Option, - pub task_kind: String, - #[serde(default)] - pub idempotency_key: Option, - #[serde(default)] - pub title: Option, - #[serde(default)] - pub prompt: Option, - #[serde(default)] - pub input: Option, - #[serde(default)] - pub expected_output: Option, - #[serde(default)] - pub required_capabilities: Vec, - #[serde(default)] - pub capability_hints: Vec, - #[serde(default)] - pub knowledge_bindings: Vec, - #[serde(default)] - pub human_review: Option, - #[serde(default)] - pub event_name: Option, - #[serde(default)] - pub turn_id: Option, - #[serde(default)] - pub provider_preference: Option, - #[serde(default)] - pub model_preference: Option, - #[serde(default)] - pub queue_if_busy: Option, - #[serde(default)] - pub skip_pre_submit_resume: Option, - #[serde(default)] - pub run_start_hooks: Option, - #[serde(default)] - pub metadata: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentAppRuntimeStartTaskResult { - pub app_id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub entry_key: Option, - pub task_id: String, - pub trace_id: String, - pub task_kind: String, - pub session_id: String, - pub turn_id: String, - pub event_name: String, - pub status: String, - pub submitted_at: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentAppRuntimeCancelTaskRequest { - pub app_id: String, - pub task_id: String, - pub session_id: String, - #[serde(default)] - pub turn_id: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentAppRuntimeCancelTaskResult { - pub app_id: String, - pub task_id: String, - pub session_id: String, - pub cancelled: bool, - pub status: String, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentAppRuntimeGetTaskRequest { - pub app_id: String, - pub task_id: String, - pub session_id: String, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentAppRuntimeTaskEvent { - pub id: String, - pub event_type: String, - pub status: String, - pub message: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub severity: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub turn_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub request_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub tool_name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub evidence_ref: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub artifact_ref: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub occurred_at: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub payload: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentAppRuntimeTaskSnapshot { - pub app_id: String, - pub task_id: String, - pub session_id: String, - pub status: String, - pub task_status: String, - pub task_events: Vec, - pub thread_read: Value, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentAppRuntimeSubmitHostResponseRequest { - pub app_id: String, - pub task_id: String, - pub runtime_request: AgentRuntimeRespondActionRequest, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentAppRuntimeSubmitHostResponseResult { - pub app_id: String, - pub task_id: String, - pub status: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct AgentAppRuntimeModelPreference { - provider_preference: String, - model_preference: String, - source: &'static str, -} - fn non_empty(value: Option<&str>) -> Option { value .map(str::trim) @@ -207,1107 +75,6 @@ fn require_text(value: Option<&str>, label: &str) -> Result { non_empty(value).ok_or_else(|| format!("{label} 不能为空")) } -fn is_unconfigured_model_preference(value: &str) -> bool { - matches!( - value.trim().to_ascii_lowercase().as_str(), - "" | "unconfigured" | "unknown" | "none" | "null" - ) -} - -fn model_preference_from_values( - provider_preference: Option, - model_preference: Option, - source: &'static str, -) -> Option { - let provider_preference = provider_preference - .and_then(|value| non_empty(Some(value.as_str()))) - .filter(|value| !is_unconfigured_model_preference(value))?; - let model_preference = model_preference - .and_then(|value| non_empty(Some(value.as_str()))) - .filter(|value| !is_unconfigured_model_preference(value))?; - - Some(AgentAppRuntimeModelPreference { - provider_preference, - model_preference, - source, - }) -} - -fn json_pointer_string(value: &Value, pointers: &[&str]) -> Option { - pointers.iter().find_map(|pointer| { - value - .pointer(pointer) - .and_then(Value::as_str) - .and_then(|value| non_empty(Some(value))) - }) -} - -fn model_preference_from_run_metadata(metadata: &Value) -> Option { - let provider_preference = json_pointer_string( - metadata, - &[ - "/turn_input/provider_routing/provider_selector", - "/turnInput/providerRouting/providerSelector", - "/request_metadata/lime_runtime/routing_decision/selected_provider", - "/request_metadata/lime_runtime/routing_decision/selectedProvider", - "/requestMetadata/limeRuntime/routingDecision/selectedProvider", - ], - ); - let model_preference = json_pointer_string( - metadata, - &[ - "/turn_input/provider_routing/model_name", - "/turnInput/providerRouting/modelName", - "/request_metadata/lime_runtime/routing_decision/selected_model", - "/request_metadata/lime_runtime/routing_decision/selectedModel", - "/requestMetadata/limeRuntime/routingDecision/selectedModel", - ], - ); - - model_preference_from_values( - provider_preference, - model_preference, - "recent_successful_agent_run", - ) -} - -fn model_preference_from_recent_successful_runs( - db: &DbConnection, -) -> Option { - let runs = { - let conn = match db.lock() { - Ok(conn) => conn, - Err(error) => { - tracing::warn!( - "[AgentAppRuntime] 读取最近模型偏好时数据库锁定失败: {}", - error - ); - return None; - } - }; - match AgentRunDao::list_runs(&conn, 50, 0) { - Ok(runs) => runs, - Err(error) => { - tracing::warn!( - "[AgentAppRuntime] 读取最近 agent_runs 失败,跳过模型偏好回填: {}", - error - ); - return None; - } - } - }; - - runs.iter() - .filter(|run| matches!(run.status, AgentRunStatus::Success)) - .find_map(model_preference_from_agent_run) -} - -fn model_preference_from_agent_run(run: &AgentRun) -> Option { - let metadata = run.metadata.as_deref()?; - let metadata: Value = serde_json::from_str(metadata).ok()?; - model_preference_from_run_metadata(&metadata) -} - -fn provider_looks_non_chat_agent_runtime_candidate(provider: &ProviderWithKeys) -> bool { - let text = [ - provider.provider.id.as_str(), - provider.provider.name.as_str(), - provider.provider.api_host.as_str(), - ] - .join(" ") - .to_ascii_lowercase(); - - text.contains("fal") - || text.contains("codex") - || text.contains("coding") - || text.contains("gpt-image") - || text.contains("gpt_images") -} - -fn model_preference_from_enabled_provider_catalog( - db: &DbConnection, - api_key_provider_service: &ApiKeyProviderServiceState, -) -> Option { - let providers = match api_key_provider_service.0.get_all_providers(db) { - Ok(providers) => providers, - Err(error) => { - tracing::warn!( - "[AgentAppRuntime] 读取 API Key Providers 失败,跳过模型偏好回填: {}", - error - ); - return None; - } - }; - - providers.into_iter().find_map(|provider| { - if !provider.provider.enabled { - return None; - } - if provider_looks_non_chat_agent_runtime_candidate(&provider) { - return None; - } - if !provider.api_keys.iter().any(|key| key.enabled) { - return None; - } - let model = provider - .provider - .custom_models - .iter() - .find_map(|model| non_empty(Some(model.as_str())))?; - model_preference_from_values( - Some(provider.provider.id), - Some(model), - "enabled_provider_custom_model", - ) - }) -} - -async fn resolve_agent_app_runtime_model_preference( - state: &AsterAgentState, - db: &DbConnection, - api_key_provider_service: &ApiKeyProviderServiceState, - request: &AgentAppRuntimeStartTaskRequest, -) -> Option { - if let Some(preference) = model_preference_from_values( - request.provider_preference.clone(), - request.model_preference.clone(), - "request", - ) { - return Some(preference); - } - - if let Some(preference) = model_preference_from_recent_successful_runs(db) { - return Some(preference); - } - - if let Some(preference) = - model_preference_from_enabled_provider_catalog(db, api_key_provider_service) - { - return Some(preference); - } - - if let Some(config) = state.get_provider_config().await { - if let Some(preference) = model_preference_from_values( - config - .provider_selector - .clone() - .or_else(|| Some(config.provider_name.clone())), - Some(config.model_name.clone()), - "current_agent_state", - ) { - return Some(preference); - } - } - - None -} - -fn insert_agent_app_runtime_model_preference_metadata( - metadata: &mut Value, - preference: &AgentAppRuntimeModelPreference, -) { - let Some(root) = metadata.as_object_mut() else { - return; - }; - let preference_value = json!({ - "provider_preference": preference.provider_preference.clone(), - "model_preference": preference.model_preference.clone(), - "source": preference.source, - }); - - let harness = root - .entry("harness".to_string()) - .or_insert_with(|| json!({})); - if let Some(harness) = harness.as_object_mut() { - harness.insert( - "agent_app_runtime_model_preference".to_string(), - preference_value.clone(), - ); - if let Some(app_runtime) = harness - .get_mut(AGENT_APP_RUNTIME_METADATA_KEY) - .and_then(Value::as_object_mut) - { - app_runtime.insert("model_preference".to_string(), preference_value.clone()); - } - } - - if let Some(app_runtime) = root - .get_mut(AGENT_APP_RUNTIME_METADATA_KEY) - .and_then(Value::as_object_mut) - { - app_runtime.insert("model_preference".to_string(), preference_value); - } -} - -fn default_task_message(request: &AgentAppRuntimeStartTaskRequest) -> String { - let title = non_empty(request.title.as_deref()) - .or_else(|| non_empty(request.prompt.as_deref())) - .unwrap_or_else(|| request.task_kind.trim().to_string()); - let input = request - .input - .as_ref() - .map(|value| serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())) - .unwrap_or_else(|| "{}".to_string()); - let expected_output = request - .expected_output - .as_ref() - .map(|value| serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())) - .unwrap_or_else(|| "{}".to_string()); - - [ - "【Agent App Runtime Task】".to_string(), - format!("App: {}", request.app_id.trim()), - format!( - "Entry: {}", - request.entry_key.as_deref().unwrap_or("default").trim() - ), - format!("TaskKind: {}", request.task_kind.trim()), - format!("Title: {title}"), - "".to_string(), - "请在 Lime AgentRuntime 主链中完成这个 App 业务任务。".to_string(), - "不要要求用户跳回通用 Chat;如需补充上下文,请通过可审计的 action / request 机制表达。" - .to_string(), - "".to_string(), - "Input JSON:".to_string(), - input, - "".to_string(), - "Expected Output JSON:".to_string(), - expected_output, - ] - .join("\n") -} - -fn expected_artifact_kind(request: &AgentAppRuntimeStartTaskRequest) -> Option { - let expected_output = request.expected_output.as_ref()?.as_object()?; - [ - "artifactKind", - "artifact_type", - "artifactType", - "kind", - "outputKind", - ] - .iter() - .filter_map(|key| expected_output.get(*key).and_then(Value::as_str)) - .find_map(|value| non_empty(Some(value))) -} - -fn is_content_factory_runtime_task(request: &AgentAppRuntimeStartTaskRequest) -> bool { - request.app_id.trim() == "content-factory-app" - || request.task_kind.trim().starts_with("content_factory.") -} - -fn build_agent_app_output_contract(request: &AgentAppRuntimeStartTaskRequest) -> Option { - if !is_content_factory_runtime_task(request) { - return None; - } - let artifact_kind = expected_artifact_kind(request)?; - Some(json!({ - "producer": "agent_runtime_artifact_metadata", - "artifact_kind": artifact_kind, - "artifact_metadata_kind": CONTENT_FACTORY_WORKSPACE_PATCH_KIND, - "patch_metadata_keys": ["contentFactoryWorkspacePatch", "workspacePatch"], - "required_patch_fields": ["kind", "projectId"], - "accepted_patch_fields": [ - "workspace", - "project", - "sceneTable", - "contentBatch", - "scripts", - "imagePrompts", - "assetPack" - ], - })) -} - -fn build_agent_app_runtime_task_message(request: &AgentAppRuntimeStartTaskRequest) -> String { - let prompt = non_empty(request.prompt.as_deref()) - .or_else(|| non_empty(request.title.as_deref())) - .unwrap_or_else(|| request.task_kind.trim().to_string()); - let input = request - .input - .as_ref() - .map(|value| serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())) - .unwrap_or_else(|| "{}".to_string()); - let expected_output = request - .expected_output - .as_ref() - .map(|value| serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())) - .unwrap_or_else(|| "{}".to_string()); - let mut lines = vec![ - "【Agent App Runtime Task】".to_string(), - format!("App: {}", request.app_id.trim()), - format!( - "Entry: {}", - request.entry_key.as_deref().unwrap_or("default").trim() - ), - format!("TaskKind: {}", request.task_kind.trim()), - "".to_string(), - "Business Prompt:".to_string(), - prompt, - "".to_string(), - "Runtime Boundary:".to_string(), - "- 请在 Lime AgentRuntime 主链中完成这个 App 业务任务。".to_string(), - "- 不要要求用户跳回通用 Chat;如需补充上下文,请通过可审计的 action / request 机制表达。" - .to_string(), - ]; - - if let Some(contract) = build_agent_app_output_contract(request) { - let artifact_kind = contract - .get("artifact_kind") - .and_then(Value::as_str) - .unwrap_or("content_batch"); - lines.extend([ - "".to_string(), - "Content Factory Output Contract:".to_string(), - format!( - "- 如果任务产出可直接物化到内容工厂项目,必须创建 artifactKind={artifact_kind} 的 artifact。" - ), - format!( - "- artifact metadata 必须包含 contentFactoryWorkspacePatch 或 workspacePatch;metadata.kind 可使用 {}。", - CONTENT_FACTORY_WORKSPACE_PATCH_KIND - ), - "- 不要通过 Bash、shell、脚本或直接写 .lime/artifacts 文件来伪造 artifact;最终回答应直接输出结构化 JSON。" - .to_string(), - "- 最终回答的顶层 JSON 必须包含 contentFactoryWorkspacePatch 或 workspacePatch,方便 Host 自动回写当前 App 页面。" - .to_string(), - "- patch 至少包含 kind / projectId,并按结果类型填写 sceneTable、contentBatch、scripts、imagePrompts 或 assetPack。" - .to_string(), - "- tools / capabilityHints 只是可选能力提示,不能把复合内容工厂任务改写成单一 research / image Skill;除非任务明确要求真实搜索或生图,否则先直接产出 workspace patch。" - .to_string(), - "- 不要只返回自然语言总结;结构化 patch 是 App 自动回写当前页面的事实源。".to_string(), - ]); - } - - lines.extend([ - "".to_string(), - "Input JSON:".to_string(), - input, - "".to_string(), - "Expected Output JSON:".to_string(), - expected_output, - ]); - - lines.join("\n") -} - -fn insert_string_if_some(map: &mut Map, key: &str, value: Option) { - if let Some(value) = value.map(|item| item.trim().to_string()) { - if !value.is_empty() { - map.insert(key.to_string(), json!(value)); - } - } -} - -fn app_task_prompt_summary(request: &AgentAppRuntimeStartTaskRequest) -> String { - non_empty(request.prompt.as_deref()) - .or_else(|| non_empty(request.title.as_deref())) - .unwrap_or_else(|| default_task_message(request)) -} - -fn build_agent_app_capability_request_context( - request: &AgentAppRuntimeStartTaskRequest, - task_id: &str, - trace_id: &str, - descriptor: AgentAppRuntimeCapabilityDescriptor, -) -> Map { - let prompt = app_task_prompt_summary(request); - let mut context = Map::new(); - - context.insert( - "source".to_string(), - json!(AGENT_APP_RUNTIME_CAPABILITY_SOURCE), - ); - context.insert("capability_id".to_string(), json!(descriptor.capability_id)); - context.insert("app_id".to_string(), json!(request.app_id.trim())); - context.insert("task_id".to_string(), json!(task_id)); - context.insert("trace_id".to_string(), json!(trace_id)); - context.insert("task_kind".to_string(), json!(request.task_kind.trim())); - context.insert( - "entry_source".to_string(), - json!(AGENT_APP_RUNTIME_CAPABILITY_SOURCE), - ); - context.insert("raw_text".to_string(), json!(prompt.clone())); - context.insert("prompt".to_string(), json!(prompt)); - context.insert( - "required_capabilities".to_string(), - json!(request.required_capabilities.clone()), - ); - context.insert( - "capability_hints".to_string(), - json!(request.capability_hints.clone()), - ); - context.insert( - "human_review".to_string(), - json!(request.human_review.unwrap_or(false)), - ); - - insert_string_if_some( - &mut context, - "entry_key", - non_empty(request.entry_key.as_deref()), - ); - insert_string_if_some( - &mut context, - "workspace_id", - non_empty(request.workspace_id.as_deref()), - ); - insert_string_if_some( - &mut context, - "idempotency_key", - non_empty(request.idempotency_key.as_deref()), - ); - - if let Some(input) = request.input.clone() { - context.insert("input".to_string(), input); - } - if let Some(expected_output) = request.expected_output.clone() { - context.insert("expected_output".to_string(), expected_output); - } - if !request.knowledge_bindings.is_empty() { - context.insert( - "knowledge_bindings".to_string(), - json!(request.knowledge_bindings.clone()), - ); - } - - match descriptor.context_key { - "image_task" | "cover_task" => { - context.insert("mode".to_string(), json!("generate")); - } - "research_request" | "report_request" => { - if !context.contains_key("query") { - let query = context - .get("prompt") - .and_then(Value::as_str) - .unwrap_or("请根据当前 App 任务补齐资料") - .to_string(); - context.insert("query".to_string(), json!(query)); - } - } - _ => {} - } - - context -} - -fn resolve_agent_app_runtime_capability_descriptors( - request: &AgentAppRuntimeStartTaskRequest, -) -> Vec { - resolve_capability_descriptors( - request - .required_capabilities - .iter() - .map(String::as_str) - .chain(request.capability_hints.iter().map(String::as_str)), - ) -} - -fn capability_descriptor_metadata(descriptor: AgentAppRuntimeCapabilityDescriptor) -> Value { - json!({ - "capability_id": descriptor.capability_id, - "skill_name": descriptor.skill_name, - "launch_key": descriptor.launch_key, - "context_key": descriptor.context_key, - "default_kind": descriptor.default_kind, - }) -} - -fn build_agent_app_capability_workflow_metadata( - request: &AgentAppRuntimeStartTaskRequest, - descriptors: &[AgentAppRuntimeCapabilityDescriptor], - output_contract: Option<&Value>, - inserts_primary_launch: bool, -) -> Option { - if descriptors.is_empty() { - return None; - } - - Some(json!({ - "source": AGENT_APP_RUNTIME_CAPABILITY_SOURCE, - "mode": if output_contract.is_some() { - "composite_output_contract" - } else if descriptors.len() > 1 { - "multi_capability" - } else { - "single_capability" - }, - "launch_policy": if inserts_primary_launch { - "primary_skill_launch" - } else { - "metadata_only" - }, - "requested_capabilities": request.required_capabilities.clone(), - "capability_hints": request.capability_hints.clone(), - "descriptors": descriptors - .iter() - .copied() - .map(capability_descriptor_metadata) - .collect::>(), - })) -} - -fn insert_agent_app_capability_launch_metadata( - root: &mut Map, - request: &AgentAppRuntimeStartTaskRequest, - task_id: &str, - trace_id: &str, - descriptor: AgentAppRuntimeCapabilityDescriptor, -) { - let launch_context = - build_agent_app_capability_request_context(request, task_id, trace_id, descriptor); - let harness = root - .entry("harness".to_string()) - .or_insert_with(|| json!({})); - let Some(harness) = harness.as_object_mut() else { - return; - }; - - harness.insert("allow_model_skills".to_string(), json!(true)); - let mut launch = Map::new(); - launch.insert("skill_name".to_string(), json!(descriptor.skill_name)); - launch.insert("kind".to_string(), json!(descriptor.default_kind)); - launch.insert( - descriptor.context_key.to_string(), - Value::Object(launch_context), - ); - harness.insert(descriptor.launch_key.to_string(), Value::Object(launch)); -} - -fn should_insert_agent_app_capability_launch_metadata( - request: &AgentAppRuntimeStartTaskRequest, - output_contract: Option<&Value>, -) -> bool { - // 内容工厂这类复合业务任务的 tools/capabilityHints 表示“可用能力”, - // 不能被提升为单一 Claw Skill 启动,否则会偏离 App 的 workspace patch 产物合同。 - if is_content_factory_runtime_task(request) && output_contract.is_some() { - return false; - } - - true -} - -fn insert_agent_app_output_contract_runtime_hints( - harness: &mut Map, - request: &AgentAppRuntimeStartTaskRequest, - output_contract: Option<&Value>, -) { - if !is_content_factory_runtime_task(request) || output_contract.is_none() { - return; - } - - // 内容工厂 patch 产出优先走直接回答 + ArtifactDocument 自动落盘, - // 避免复合业务任务被 FullRuntime 的通用工具链带偏成读文件 / Bash / 子代理循环。 - harness - .entry("chat_mode".to_string()) - .or_insert_with(|| json!("general")); - harness - .entry("session_mode".to_string()) - .or_insert_with(|| json!("general_workbench")); -} - -fn push_task_event( - events: &mut Vec, - event_type: &str, - status: &str, - message: impl Into, - occurred_at: Option, - payload: Option, -) { - events.push(AgentAppRuntimeTaskEvent { - id: format!("{}:{}", event_type, events.len() + 1), - event_type: event_type.to_string(), - status: status.to_string(), - message: message.into(), - severity: None, - turn_id: None, - request_id: None, - tool_name: None, - evidence_ref: None, - artifact_ref: None, - occurred_at, - payload, - }); -} - -fn outcome_event_type(outcome_type: &str) -> &'static str { - let normalized = outcome_type.to_ascii_lowercase(); - if normalized.contains("cancel") || normalized.contains("interrupt") { - "task:cancelled" - } else if normalized.contains("fail") - || normalized.contains("error") - || normalized.contains("timeout") - { - "task:error" - } else { - "task:completed" - } -} - -fn has_missing_context(context_summary: Option<&Value>) -> Option { - let summary = context_summary?.as_object()?; - let missing_context = summary - .get("missing_context") - .or_else(|| summary.get("missingContext"))?; - if missing_context - .as_array() - .is_some_and(|items| !items.is_empty()) - { - Some(missing_context.clone()) - } else { - None - } -} - -fn is_content_factory_workspace_patch_kind(value: &str) -> bool { - matches!( - value.trim(), - CONTENT_FACTORY_WORKSPACE_PATCH_KIND - | "contentFactoryWorkspacePatch" - | "workspace_patch" - | "workspacePatch" - ) -} - -fn has_content_factory_workspace_patch_fields(value: &Value) -> bool { - value.as_object().is_some_and(|object| { - object.contains_key("workspace") - || object.contains_key("project") - || object.contains_key("sceneTable") - || object.contains_key("contentBatch") - || object.contains_key("scripts") - || object.contains_key("imagePrompts") - || object.contains_key("assetPack") - }) -} - -fn extract_content_factory_workspace_patch(metadata: Option<&Value>) -> Option { - let metadata = metadata?; - for key in ["contentFactoryWorkspacePatch", "workspacePatch"] { - if let Some(value) = metadata.get(key) { - if has_content_factory_workspace_patch_fields(value) { - return Some(value.clone()); - } - } - } - - let artifact_kind = metadata - .get("artifactType") - .or_else(|| metadata.get("artifact_type")) - .or_else(|| metadata.get("kind")) - .or_else(|| metadata.get("outputKind")) - .and_then(Value::as_str); - if artifact_kind.is_some_and(is_content_factory_workspace_patch_kind) - && has_content_factory_workspace_patch_fields(metadata) - { - return Some(metadata.clone()); - } - - None -} - -fn parse_json_object_from_markdown(value: &str) -> Option { - let trimmed = value.trim(); - let candidate = if trimmed.starts_with("```") { - let without_opening = trimmed.lines().skip(1).collect::>().join("\n"); - without_opening - .rsplit_once("```") - .map(|(body, _)| body.trim().to_string()) - .unwrap_or(without_opening) - } else { - trimmed.to_string() - }; - - serde_json::from_str::(&candidate).ok().or_else(|| { - let start = candidate.find('{')?; - let end = candidate.rfind('}')?; - serde_json::from_str::(&candidate[start..=end]).ok() - }) -} - -fn extract_content_factory_workspace_patch_from_artifact_document( - metadata: Option<&Value>, -) -> Option { - let metadata = metadata?; - let artifact_document = metadata - .get("artifactDocument") - .or_else(|| metadata.get("artifact_document"))?; - let blocks = artifact_document.get("blocks")?.as_array()?; - - blocks.iter().find_map(|block| { - let text = block - .get("content") - .or_else(|| block.get("markdown")) - .and_then(Value::as_str)?; - let parsed = parse_json_object_from_markdown(text)?; - extract_content_factory_workspace_patch(Some(&parsed)) - }) -} - -fn build_artifact_event_payload(artifact: &AgentRuntimeThreadArtifactView) -> Option { - let artifact_value = serde_json::to_value(artifact).ok()?; - let workspace_patch = extract_content_factory_workspace_patch(artifact.metadata.as_ref()) - .or_else(|| { - extract_content_factory_workspace_patch_from_artifact_document( - artifact.metadata.as_ref(), - ) - }); - if let Some(workspace_patch) = workspace_patch { - return Some(json!({ - "artifact": artifact_value, - "workspacePatch": workspace_patch, - "contentFactoryWorkspacePatch": workspace_patch, - "producer": "agent_runtime_artifact_metadata", - })); - } - Some(artifact_value) -} - -fn build_agent_app_runtime_task_events( - thread_read: &AgentRuntimeThreadReadModel, -) -> Vec { - let mut events = Vec::new(); - - for queued_turn in &thread_read.queued_turns { - push_task_event( - &mut events, - "task:queued", - "queued", - queued_turn.message_preview.clone(), - None, - serde_json::to_value(queued_turn).ok(), - ); - } - - let status_message = match thread_read.profile_status.as_str() { - "idle" => "任务已接收,等待 AgentRuntime 调度或回写进度".to_string(), - "queued" => "任务已进入队列".to_string(), - "running" => "任务正在执行".to_string(), - "blocked" => "任务等待用户或权限响应".to_string(), - "completed" => "任务已完成".to_string(), - "failed" => "任务执行失败".to_string(), - "cancelled" => "任务已取消".to_string(), - _ => format!("任务状态:{}", thread_read.status), - }; - push_task_event( - &mut events, - "task:progress", - thread_read.profile_status.as_str(), - status_message, - thread_read.updated_at.clone(), - Some(json!({ - "thread_id": thread_read.thread_id.clone(), - "active_turn_id": thread_read.active_turn_id.clone(), - "profile_status": thread_read.profile_status.clone(), - "status": thread_read.status.clone(), - })), - ); - - for pending_request in &thread_read.pending_requests { - let message = pending_request - .title - .clone() - .unwrap_or_else(|| "任务等待 Host / 用户响应".to_string()); - let mut event = AgentAppRuntimeTaskEvent { - id: format!("task:reviewRequested:{}", pending_request.id), - event_type: "task:reviewRequested".to_string(), - status: pending_request.status.clone(), - message, - severity: None, - turn_id: pending_request.turn_id.clone(), - request_id: Some(pending_request.id.clone()), - tool_name: None, - evidence_ref: None, - artifact_ref: None, - occurred_at: pending_request.created_at.clone(), - payload: serde_json::to_value(pending_request).ok(), - }; - if matches!( - pending_request.request_type.as_str(), - "missing_context" | "ask_user" | "elicitation" - ) { - event.event_type = "task:missingContextRequested".to_string(); - } - events.push(event); - } - - if let Some(missing_context) = has_missing_context(thread_read.context_summary.as_ref()) { - push_task_event( - &mut events, - "task:missingContextRequested", - "blocked", - "任务需要补齐上下文", - thread_read.updated_at.clone(), - Some(json!({ "missing_context": missing_context })), - ); - } - - for tool_call in &thread_read.tool_calls { - events.push(AgentAppRuntimeTaskEvent { - id: format!("task:toolCall:{}", tool_call.tool_call_id), - event_type: "task:toolCall".to_string(), - status: tool_call.status.clone(), - message: format!("工具 {} {}", tool_call.tool_name, tool_call.status), - severity: if tool_call.success == Some(false) { - Some("warning".to_string()) - } else { - None - }, - turn_id: Some(tool_call.turn_id.clone()), - request_id: None, - tool_name: Some(tool_call.tool_name.clone()), - evidence_ref: None, - artifact_ref: None, - occurred_at: None, - payload: serde_json::to_value(tool_call).ok(), - }); - } - - for artifact in &thread_read.artifacts { - let payload = build_artifact_event_payload(artifact); - events.push(AgentAppRuntimeTaskEvent { - id: format!("artifact:created:{}", artifact.item_id), - event_type: "artifact:created".to_string(), - status: artifact.status.clone(), - message: artifact - .title - .clone() - .unwrap_or_else(|| format!("Artifact 已创建:{}", artifact.path)), - severity: (artifact.status == "failed").then(|| "error".to_string()), - turn_id: Some(artifact.turn_id.clone()), - request_id: None, - tool_name: None, - evidence_ref: None, - artifact_ref: Some(artifact.path.clone()), - occurred_at: artifact - .completed_at - .clone() - .or_else(|| artifact.updated_at.clone()) - .or_else(|| artifact.created_at.clone()), - payload, - }); - } - - for evidence_ref in &thread_read.evidence_summary.evidence_refs { - events.push(AgentAppRuntimeTaskEvent { - id: format!("evidence:recorded:{evidence_ref}"), - event_type: "evidence:recorded".to_string(), - status: "recorded".to_string(), - message: "运行证据已记录".to_string(), - severity: None, - turn_id: None, - request_id: None, - tool_name: None, - evidence_ref: Some(evidence_ref.clone()), - artifact_ref: None, - occurred_at: thread_read.updated_at.clone(), - payload: None, - }); - } - - for (index, outcome) in thread_read - .evidence_summary - .verification_outcomes - .iter() - .enumerate() - { - push_task_event( - &mut events, - "evidence:verified", - "verified", - "运行证据已验证", - thread_read.updated_at.clone(), - Some(json!({ "index": index, "outcome": outcome })), - ); - } - - if let Some(outcome) = &thread_read.last_outcome { - let event_type = outcome_event_type(&outcome.outcome_type); - events.push(AgentAppRuntimeTaskEvent { - id: format!( - "{}:{}", - event_type, - outcome.turn_id.as_deref().unwrap_or("latest") - ), - event_type: event_type.to_string(), - status: outcome.outcome_type.clone(), - message: outcome - .summary - .clone() - .or_else(|| outcome.primary_cause.clone()) - .unwrap_or_else(|| "任务回合已结束".to_string()), - severity: (event_type == "task:error").then(|| "error".to_string()), - turn_id: outcome.turn_id.clone(), - request_id: None, - tool_name: None, - evidence_ref: None, - artifact_ref: None, - occurred_at: outcome.ended_at.clone(), - payload: serde_json::to_value(outcome).ok(), - }); - } - - for incident in &thread_read.incidents { - events.push(AgentAppRuntimeTaskEvent { - id: format!("task:incident:{}", incident.id), - event_type: "task:incident".to_string(), - status: incident.status.clone(), - message: incident.title.clone(), - severity: Some(incident.severity.clone()), - turn_id: incident.turn_id.clone(), - request_id: None, - tool_name: None, - evidence_ref: None, - artifact_ref: None, - occurred_at: incident.detected_at.clone(), - payload: serde_json::to_value(incident).ok(), - }); - } - - events -} - -fn build_agent_app_runtime_task_snapshot_event_payload( - snapshot: &AgentAppRuntimeTaskSnapshot, -) -> Value { - let snapshot_value = serde_json::to_value(snapshot).unwrap_or_else(|_| json!({})); - json!({ - "type": "agent_app_runtime:taskSnapshot", - "eventType": "task:update", - "appId": snapshot.app_id.clone(), - "taskId": snapshot.task_id.clone(), - "sessionId": snapshot.session_id.clone(), - "taskStatus": snapshot.task_status.clone(), - "status": snapshot.status.clone(), - "task": snapshot_value.clone(), - "snapshot": snapshot_value, - "taskEvents": snapshot.task_events.clone(), - "threadRead": snapshot.thread_read.clone(), - "emittedAt": Utc::now().to_rfc3339(), - }) -} - -fn emit_agent_app_runtime_task_snapshot(app: &AppHandle, snapshot: &AgentAppRuntimeTaskSnapshot) { - let event_name = agent_app_runtime_event_name(&snapshot.app_id, &snapshot.task_id); - let payload = build_agent_app_runtime_task_snapshot_event_payload(snapshot); - if let Err(error) = app.emit(&event_name, payload) { - tracing::warn!( - "[AgentAppRuntime] 发送 App task projection event 失败: event_name={}, error={}", - event_name, - error - ); - } -} - -fn build_agent_app_runtime_metadata( - request: &AgentAppRuntimeStartTaskRequest, - task_id: &str, - trace_id: &str, -) -> Value { - let mut metadata = request.metadata.clone().unwrap_or_else(|| json!({})); - if !metadata.is_object() { - metadata = json!({}); - } - - let mut app_runtime = json!({ - "surface": "agent_app", - "app_id": request.app_id.trim(), - "entry_key": request.entry_key.as_deref().unwrap_or("").trim(), - "task_id": task_id, - "trace_id": trace_id, - "task_kind": request.task_kind.trim(), - "idempotency_key": request.idempotency_key.as_deref().unwrap_or("").trim(), - "required_capabilities": request.required_capabilities.clone(), - "capability_hints": request.capability_hints.clone(), - "knowledge_bindings": request.knowledge_bindings.clone(), - "human_review": request.human_review.unwrap_or(false), - }); - let output_contract = build_agent_app_output_contract(request); - let capability_descriptors = resolve_agent_app_runtime_capability_descriptors(request); - let should_insert_primary_capability_launch = - should_insert_agent_app_capability_launch_metadata(request, output_contract.as_ref()) - && !capability_descriptors.is_empty(); - let capability_workflow = build_agent_app_capability_workflow_metadata( - request, - &capability_descriptors, - output_contract.as_ref(), - should_insert_primary_capability_launch, - ); - if let (Some(app_runtime), Some(output_contract)) = - (app_runtime.as_object_mut(), output_contract.clone()) - { - app_runtime.insert("output_contract".to_string(), output_contract); - } - if let (Some(app_runtime), Some(capability_workflow)) = - (app_runtime.as_object_mut(), capability_workflow.clone()) - { - app_runtime.insert("capability_workflow".to_string(), capability_workflow); - } - - if let Some(root) = metadata.as_object_mut() { - root.insert( - AGENT_APP_RUNTIME_METADATA_KEY.to_string(), - app_runtime.clone(), - ); - let lime_runtime = root - .entry(LIME_RUNTIME_METADATA_KEY.to_string()) - .or_insert_with(|| json!({})); - if let Some(lime_runtime) = lime_runtime.as_object_mut() { - lime_runtime.insert("surface".to_string(), json!("agent_app")); - lime_runtime.insert("app_id".to_string(), json!(request.app_id.trim())); - lime_runtime.insert("task_id".to_string(), json!(task_id)); - lime_runtime.insert("trace_id".to_string(), json!(trace_id)); - lime_runtime.insert("task_kind".to_string(), json!(request.task_kind.trim())); - let runtime_summary = lime_runtime - .entry("runtime_summary".to_string()) - .or_insert_with(|| json!({})); - if let Some(runtime_summary) = runtime_summary.as_object_mut() { - runtime_summary.insert("surface".to_string(), json!("agent_app")); - runtime_summary.insert("app_id".to_string(), json!(request.app_id.trim())); - runtime_summary.insert("task_id".to_string(), json!(task_id)); - runtime_summary.insert("trace_id".to_string(), json!(trace_id)); - runtime_summary.insert("task_kind".to_string(), json!(request.task_kind.trim())); - } - } - { - let harness = root - .entry("harness".to_string()) - .or_insert_with(|| json!({})); - if let Some(harness) = harness.as_object_mut() { - harness.insert( - AGENT_APP_RUNTIME_METADATA_KEY.to_string(), - app_runtime.clone(), - ); - if let Some(output_contract) = output_contract.clone() { - harness.insert( - "agent_app_runtime_output_contract".to_string(), - output_contract, - ); - } - insert_agent_app_output_contract_runtime_hints( - harness, - request, - output_contract.as_ref(), - ); - if let Some(capability_workflow) = capability_workflow.clone() { - harness.insert( - "agent_app_runtime_capability_workflow".to_string(), - capability_workflow, - ); - } - } - } - if should_insert_primary_capability_launch { - if let Some(descriptor) = capability_descriptors.first().copied() { - insert_agent_app_capability_launch_metadata( - root, request, task_id, trace_id, descriptor, - ); - } - } - } - - metadata -} - #[allow(clippy::too_many_arguments)] #[tauri::command] pub async fn agent_app_runtime_start_task( @@ -1520,493 +287,3 @@ pub async fn agent_app_runtime_submit_host_response( status: "submitted".to_string(), }) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::agent::QueuedTurnSnapshot; - use crate::commands::aster_agent_cmd::{ - AgentRuntimeIncidentView, AgentRuntimeOutcomeView, AgentRuntimeRequestView, - AgentRuntimeThreadArtifactView, AgentRuntimeThreadEvidenceSummary, - AgentRuntimeThreadTelemetrySummary, AgentRuntimeThreadToolCallView, - }; - - fn runtime_request( - required_capabilities: Vec<&str>, - capability_hints: Vec<&str>, - ) -> AgentAppRuntimeStartTaskRequest { - AgentAppRuntimeStartTaskRequest { - app_id: "content-factory-app".to_string(), - entry_key: Some("content_factory".to_string()), - workspace_id: Some("workspace-1".to_string()), - session_id: None, - task_id: None, - task_kind: "content_factory.copy.generate".to_string(), - idempotency_key: None, - title: Some("生成小红书种草文案".to_string()), - prompt: Some("围绕春季护肤新品生成文案,并补齐资料来源".to_string()), - input: Some(json!({ - "platform": "xiaohongshu", - "audience": "敏感肌用户" - })), - expected_output: Some(json!({ - "artifacts": ["copy", "assetBrief"] - })), - required_capabilities: required_capabilities - .into_iter() - .map(str::to_string) - .collect(), - capability_hints: capability_hints.into_iter().map(str::to_string).collect(), - knowledge_bindings: Vec::new(), - human_review: Some(true), - event_name: None, - turn_id: None, - provider_preference: None, - model_preference: None, - queue_if_busy: None, - skip_pre_submit_resume: None, - run_start_hooks: None, - metadata: None, - } - } - - fn base_thread_read() -> AgentRuntimeThreadReadModel { - AgentRuntimeThreadReadModel { - thread_id: "thread-1".to_string(), - status: "running".to_string(), - profile_status: "running".to_string(), - active_turn_id: Some("turn-1".to_string()), - turns: Vec::new(), - pending_requests: Vec::new(), - last_outcome: None, - incidents: Vec::new(), - queued_turns: Vec::new(), - tool_calls: Vec::new(), - artifacts: Vec::new(), - model_routing: None, - evidence_summary: AgentRuntimeThreadEvidenceSummary::default(), - telemetry_summary: AgentRuntimeThreadTelemetrySummary::default(), - context_summary: None, - interrupt_state: None, - updated_at: Some("2026-05-16T00:00:00.000Z".to_string()), - latest_compaction_boundary: None, - file_checkpoint_summary: None, - diagnostics: None, - task_kind: None, - service_model_slot: None, - routing_mode: None, - decision_source: None, - decision_reason: None, - candidate_count: None, - fallback_chain: None, - capability_gap: None, - estimated_cost_class: None, - single_candidate_only: None, - oem_policy: None, - runtime_summary: None, - auxiliary_task_runtime: None, - limit_state: None, - cost_state: None, - permission_state: None, - limit_event: None, - } - } - - #[test] - fn test_agent_app_runtime_session_id_uses_hidden_prefix() { - assert!(new_agent_app_runtime_session_id().starts_with(AGENT_APP_RUNTIME_SESSION_ID_PREFIX)); - } - - #[test] - fn test_agent_app_runtime_model_preference_reads_recent_successful_routing_metadata() { - let metadata = json!({ - "request_metadata": { - "lime_runtime": { - "routing_decision": { - "selected_provider": "deepseek", - "selected_model": "deepseek-v4-flash" - } - } - } - }); - - let preference = - model_preference_from_run_metadata(&metadata).expect("recent run preference"); - - assert_eq!(preference.provider_preference, "deepseek"); - assert_eq!(preference.model_preference, "deepseek-v4-flash"); - assert_eq!(preference.source, "recent_successful_agent_run"); - } - - #[test] - fn test_agent_app_runtime_metadata_maps_research_capability_to_claw_launch() { - let request = runtime_request( - vec!["text_generation", "lime.capability.research.search"], - Vec::new(), - ); - let metadata = build_agent_app_runtime_metadata(&request, "task-1", "trace-1"); - let harness = metadata - .get("harness") - .and_then(Value::as_object) - .expect("harness metadata"); - let launch = harness - .get("research_skill_launch") - .and_then(Value::as_object) - .expect("research launch"); - let research_request = launch - .get("research_request") - .and_then(Value::as_object) - .expect("research request"); - let runtime_summary = metadata - .get("lime_runtime") - .and_then(|value| value.get("runtime_summary")) - .and_then(Value::as_object) - .expect("agent app runtime summary"); - - assert_eq!(harness.get("allow_model_skills"), Some(&json!(true))); - assert!(harness.get("agent_app_runtime").is_some()); - assert_eq!(runtime_summary.get("surface"), Some(&json!("agent_app"))); - assert_eq!( - runtime_summary.get("app_id"), - Some(&json!("content-factory-app")) - ); - assert_eq!(runtime_summary.get("task_id"), Some(&json!("task-1"))); - assert_eq!(runtime_summary.get("trace_id"), Some(&json!("trace-1"))); - assert_eq!(launch.get("skill_name"), Some(&json!("research"))); - assert_eq!(launch.get("kind"), Some(&json!("research_request"))); - assert_eq!( - research_request.get("source"), - Some(&json!("agent_app_runtime")) - ); - assert_eq!( - research_request.get("app_id"), - Some(&json!("content-factory-app")) - ); - assert_eq!( - research_request.get("capability_id"), - Some(&json!("lime.capability.research.search")) - ); - assert!(research_request.get("query").is_some()); - } - - #[test] - fn test_agent_app_runtime_metadata_maps_image_alias_to_claw_launch() { - let request = runtime_request(Vec::new(), vec!["image_generation"]); - let metadata = build_agent_app_runtime_metadata(&request, "task-1", "trace-1"); - let launch = metadata - .get("harness") - .and_then(Value::as_object) - .and_then(|harness| harness.get("image_skill_launch")) - .and_then(Value::as_object) - .expect("image launch"); - let image_task = launch - .get("image_task") - .and_then(Value::as_object) - .expect("image task"); - - assert_eq!(launch.get("skill_name"), Some(&json!("image_generate"))); - assert_eq!(launch.get("kind"), Some(&json!("image_task"))); - assert_eq!(image_task.get("mode"), Some(&json!("generate"))); - assert_eq!( - image_task.get("entry_source"), - Some(&json!("agent_app_runtime")) - ); - } - - #[test] - fn test_agent_app_runtime_metadata_ignores_unknown_capability_without_fake_launch() { - let request = runtime_request(vec!["text_generation"], Vec::new()); - let metadata = build_agent_app_runtime_metadata(&request, "task-1", "trace-1"); - let harness = metadata - .get("harness") - .and_then(Value::as_object) - .expect("harness metadata"); - - assert!(harness.get("agent_app_runtime").is_some()); - assert!(harness.get("allow_model_skills").is_none()); - assert!(harness.get("image_skill_launch").is_none()); - assert!(harness.get("research_skill_launch").is_none()); - } - - #[test] - fn test_agent_app_runtime_content_factory_output_contract_is_machine_readable() { - let mut request = runtime_request(Vec::new(), Vec::new()); - request.expected_output = Some(json!({ - "artifactKind": "content_batch", - "includes": ["copy", "script", "image_brief"] - })); - - let metadata = build_agent_app_runtime_metadata(&request, "task-1", "trace-1"); - let harness = metadata - .get("harness") - .and_then(Value::as_object) - .expect("harness metadata"); - let output_contract = harness - .get("agent_app_runtime_output_contract") - .and_then(Value::as_object) - .expect("output contract"); - assert_eq!( - output_contract.get("artifact_kind"), - Some(&json!("content_batch")) - ); - assert_eq!( - output_contract.get("artifact_metadata_kind"), - Some(&json!(CONTENT_FACTORY_WORKSPACE_PATCH_KIND)) - ); - assert!(output_contract - .get("patch_metadata_keys") - .and_then(Value::as_array) - .is_some_and(|items| items.contains(&json!("contentFactoryWorkspacePatch")))); - - let message = build_agent_app_runtime_task_message(&request); - assert!(message.contains("Content Factory Output Contract")); - assert!(message.contains("contentFactoryWorkspacePatch")); - assert!(message.contains("artifactKind=content_batch")); - assert!(message.contains("不要通过 Bash")); - assert!(message.contains("不能把复合内容工厂任务改写成单一 research / image Skill")); - } - - #[test] - fn test_agent_app_runtime_content_factory_output_contract_does_not_force_single_skill_launch() { - let mut request = runtime_request(Vec::new(), vec!["research.search", "image_generation"]); - request.expected_output = Some(json!({ - "artifactKind": "content_batch", - "includes": ["copy", "script", "image_brief"] - })); - - let metadata = build_agent_app_runtime_metadata(&request, "task-1", "trace-1"); - let harness = metadata - .get("harness") - .and_then(Value::as_object) - .expect("harness metadata"); - - assert!(harness.get("agent_app_runtime_output_contract").is_some()); - assert_eq!(harness.get("chat_mode"), Some(&json!("general"))); - assert_eq!( - harness.get("session_mode"), - Some(&json!("general_workbench")) - ); - let workflow = harness - .get("agent_app_runtime_capability_workflow") - .and_then(Value::as_object) - .expect("capability workflow"); - assert_eq!( - workflow.get("mode"), - Some(&json!("composite_output_contract")) - ); - assert_eq!(workflow.get("launch_policy"), Some(&json!("metadata_only"))); - let descriptors = workflow - .get("descriptors") - .and_then(Value::as_array) - .expect("workflow descriptors"); - assert_eq!(descriptors.len(), 2); - assert!(descriptors - .iter() - .any(|descriptor| descriptor.get("capability_id") - == Some(&json!("lime.capability.research.search")))); - assert!(descriptors - .iter() - .any(|descriptor| descriptor.get("capability_id") - == Some(&json!("lime.capability.image.generate")))); - assert!(harness.get("allow_model_skills").is_none()); - assert!(harness.get("research_skill_launch").is_none()); - assert!(harness.get("image_skill_launch").is_none()); - } - - #[test] - fn test_agent_app_runtime_extracts_workspace_patch_from_artifact_document_blocks() { - let metadata = json!({ - "artifactDocument": { - "blocks": [ - { - "type": "rich_text", - "content": "```json\n{\"contentFactoryWorkspacePatch\":{\"kind\":\"content_batch\",\"projectId\":\"project-1\",\"contentBatch\":{\"items\":[{\"title\":\"示例文案\"}]}}}\n```" - } - ] - } - }); - - let patch = extract_content_factory_workspace_patch_from_artifact_document(Some(&metadata)) - .expect("workspace patch"); - - assert_eq!(patch.get("kind"), Some(&json!("content_batch"))); - assert_eq!(patch.get("projectId"), Some(&json!("project-1"))); - assert!(patch.get("contentBatch").is_some()); - } - - #[test] - fn test_agent_app_runtime_task_events_project_thread_read_facts() { - let mut thread_read = base_thread_read(); - thread_read.queued_turns = vec![QueuedTurnSnapshot { - queued_turn_id: "queued-1".to_string(), - message_preview: "排队任务".to_string(), - message_text: "排队任务完整文本".to_string(), - created_at: 1_789_000_000, - image_count: 0, - position: 0, - }]; - thread_read.pending_requests = vec![AgentRuntimeRequestView { - id: "request-1".to_string(), - thread_id: "thread-1".to_string(), - turn_id: Some("turn-1".to_string()), - item_id: None, - request_type: "ask_user".to_string(), - status: "pending".to_string(), - title: Some("需要确认素材方向".to_string()), - payload: Some(json!({ "question": "是否继续?" })), - decision: None, - scope: None, - created_at: Some("2026-05-16T00:00:01.000Z".to_string()), - resolved_at: None, - }]; - thread_read.context_summary = Some(json!({ - "missing_context": [{ "field": "target_audience" }] - })); - thread_read.tool_calls = vec![AgentRuntimeThreadToolCallView { - tool_call_id: "tool-1".to_string(), - turn_id: "turn-1".to_string(), - tool_name: "Skill(research)".to_string(), - status: "completed".to_string(), - success: Some(true), - error: None, - }]; - thread_read.artifacts = vec![AgentRuntimeThreadArtifactView { - item_id: "artifact-item-1".to_string(), - turn_id: "turn-1".to_string(), - path: ".lime/artifacts/content-batch.json".to_string(), - source: "agent_runtime".to_string(), - status: "created".to_string(), - artifact_type: Some("content_batch".to_string()), - title: Some("内容批次".to_string()), - created_at: Some("2026-05-16T00:00:01.500Z".to_string()), - completed_at: Some("2026-05-16T00:00:01.800Z".to_string()), - updated_at: Some("2026-05-16T00:00:01.800Z".to_string()), - metadata: Some(json!({ - "artifactType": "content_batch", - "workspacePatch": { - "kind": "content_batch", - "projectId": "project-1", - "contentBatch": { "count": 20 } - } - })), - }]; - thread_read.evidence_summary = AgentRuntimeThreadEvidenceSummary { - evidence_refs: vec!["evidence-1".to_string()], - verification_outcomes: vec![json!({ "status": "passed" })], - }; - thread_read.last_outcome = Some(AgentRuntimeOutcomeView { - thread_id: "thread-1".to_string(), - turn_id: Some("turn-1".to_string()), - outcome_type: "completed".to_string(), - summary: Some("任务完成".to_string()), - primary_cause: None, - retryable: false, - ended_at: Some("2026-05-16T00:00:02.000Z".to_string()), - }); - thread_read.incidents = vec![AgentRuntimeIncidentView { - id: "incident-1".to_string(), - thread_id: "thread-1".to_string(), - turn_id: Some("turn-1".to_string()), - item_id: None, - incident_type: "provider_warning".to_string(), - severity: "medium".to_string(), - status: "open".to_string(), - title: "Provider warning".to_string(), - details: None, - detected_at: Some("2026-05-16T00:00:03.000Z".to_string()), - cleared_at: None, - }]; - - let events = build_agent_app_runtime_task_events(&thread_read); - let event_types = events - .iter() - .map(|event| event.event_type.as_str()) - .collect::>(); - - assert!(event_types.contains(&"task:queued")); - assert!(event_types.contains(&"task:progress")); - assert!(event_types.contains(&"task:missingContextRequested")); - assert!(event_types.contains(&"task:toolCall")); - assert!(event_types.contains(&"artifact:created")); - assert!(event_types.contains(&"evidence:recorded")); - assert!(event_types.contains(&"evidence:verified")); - assert!(event_types.contains(&"task:completed")); - assert!(event_types.contains(&"task:incident")); - assert!(events - .iter() - .any(|event| event.request_id.as_deref() == Some("request-1"))); - assert!(events - .iter() - .any(|event| event.evidence_ref.as_deref() == Some("evidence-1"))); - assert!(events.iter().any( - |event| event.artifact_ref.as_deref() == Some(".lime/artifacts/content-batch.json") - )); - let artifact_event = events - .iter() - .find(|event| event.event_type == "artifact:created") - .expect("artifact event"); - assert_eq!( - artifact_event - .payload - .as_ref() - .and_then(|payload| payload.get("contentFactoryWorkspacePatch")) - .and_then(|patch| patch.get("contentBatch")) - .and_then(|content_batch| content_batch.get("count")), - Some(&json!(20)) - ); - } - - #[test] - fn test_agent_app_runtime_task_snapshot_event_payload_is_canonical() { - let mut thread_read = base_thread_read(); - thread_read.profile_status = "running".to_string(); - let task_events = build_agent_app_runtime_task_events(&thread_read); - let snapshot = AgentAppRuntimeTaskSnapshot { - app_id: "content-factory-app".to_string(), - task_id: "task-1".to_string(), - session_id: "session-1".to_string(), - status: "thread_read_available".to_string(), - task_status: thread_read.profile_status.clone(), - task_events, - thread_read: serde_json::to_value(&thread_read).expect("thread read value"), - }; - - let payload = build_agent_app_runtime_task_snapshot_event_payload(&snapshot); - - assert_eq!( - payload.get("type"), - Some(&json!("agent_app_runtime:taskSnapshot")) - ); - assert_eq!(payload.get("eventType"), Some(&json!("task:update"))); - assert_eq!(payload.get("taskId"), Some(&json!("task-1"))); - assert_eq!( - payload - .get("taskEvents") - .and_then(Value::as_array) - .map(Vec::len), - Some(snapshot.task_events.len()) - ); - assert!(payload.get("threadRead").is_some()); - assert!(payload.get("task").is_some()); - } - - #[test] - fn test_agent_app_runtime_idle_status_uses_business_progress_copy() { - let mut thread_read = base_thread_read(); - thread_read.status = "idle".to_string(); - thread_read.profile_status = "idle".to_string(); - thread_read.active_turn_id = None; - - let events = build_agent_app_runtime_task_events(&thread_read); - let progress = events - .iter() - .find(|event| event.event_type == "task:progress") - .expect("progress event"); - - assert_eq!( - progress.message, - "任务已接收,等待 AgentRuntime 调度或回写进度" - ); - assert_ne!(progress.message, "任务状态:idle"); - } -} diff --git a/src-tauri/src/commands/agent_app_runtime_cmd/events.rs b/src-tauri/src/commands/agent_app_runtime_cmd/events.rs new file mode 100644 index 000000000..886ac879a --- /dev/null +++ b/src-tauri/src/commands/agent_app_runtime_cmd/events.rs @@ -0,0 +1,437 @@ +use super::types::{AgentAppRuntimeTaskEvent, AgentAppRuntimeTaskSnapshot}; +use super::{agent_app_runtime_event_name, CONTENT_FACTORY_WORKSPACE_PATCH_KIND}; +use crate::commands::aster_agent_cmd::{ + AgentRuntimeThreadArtifactView, AgentRuntimeThreadReadModel, +}; +use chrono::Utc; +use serde_json::{json, Value}; +use tauri::{AppHandle, Emitter}; + +fn push_task_event( + events: &mut Vec, + event_type: &str, + status: &str, + message: impl Into, + occurred_at: Option, + payload: Option, +) { + events.push(AgentAppRuntimeTaskEvent { + id: format!("{}:{}", event_type, events.len() + 1), + event_type: event_type.to_string(), + status: status.to_string(), + message: message.into(), + severity: None, + turn_id: None, + request_id: None, + tool_name: None, + evidence_ref: None, + artifact_ref: None, + occurred_at, + payload, + }); +} + +fn outcome_event_type(outcome_type: &str) -> &'static str { + let normalized = outcome_type.to_ascii_lowercase(); + if normalized.contains("cancel") || normalized.contains("interrupt") { + "task:cancelled" + } else if normalized.contains("fail") + || normalized.contains("error") + || normalized.contains("timeout") + { + "task:error" + } else { + "task:completed" + } +} + +fn has_missing_context(context_summary: Option<&Value>) -> Option { + let summary = context_summary?.as_object()?; + let missing_context = summary + .get("missing_context") + .or_else(|| summary.get("missingContext"))?; + if missing_context + .as_array() + .is_some_and(|items| !items.is_empty()) + { + Some(missing_context.clone()) + } else { + None + } +} + +fn is_content_factory_workspace_patch_kind(value: &str) -> bool { + matches!( + value.trim(), + CONTENT_FACTORY_WORKSPACE_PATCH_KIND + | "contentFactoryWorkspacePatch" + | "workspace_patch" + | "workspacePatch" + ) +} + +fn has_content_factory_workspace_patch_fields(value: &Value) -> bool { + value.as_object().is_some_and(|object| { + object.contains_key("workspace") + || object.contains_key("project") + || object.contains_key("sceneTable") + || object.contains_key("contentBatch") + || object.contains_key("scripts") + || object.contains_key("imagePrompts") + || object.contains_key("assetPack") + }) +} + +fn extract_content_factory_workspace_patch(metadata: Option<&Value>) -> Option { + let metadata = metadata?; + for key in ["contentFactoryWorkspacePatch", "workspacePatch"] { + if let Some(value) = metadata.get(key) { + if has_content_factory_workspace_patch_fields(value) { + return Some(value.clone()); + } + } + } + + let artifact_kind = metadata + .get("artifactType") + .or_else(|| metadata.get("artifact_type")) + .or_else(|| metadata.get("kind")) + .or_else(|| metadata.get("outputKind")) + .and_then(Value::as_str); + if artifact_kind.is_some_and(is_content_factory_workspace_patch_kind) + && has_content_factory_workspace_patch_fields(metadata) + { + return Some(metadata.clone()); + } + + None +} + +fn parse_json_object_from_markdown(value: &str) -> Option { + let trimmed = value.trim(); + let candidate = if trimmed.starts_with("```") { + let without_opening = trimmed.lines().skip(1).collect::>().join("\n"); + without_opening + .rsplit_once("```") + .map(|(body, _)| body.trim().to_string()) + .unwrap_or(without_opening) + } else { + trimmed.to_string() + }; + + serde_json::from_str::(&candidate).ok().or_else(|| { + let start = candidate.find('{')?; + let end = candidate.rfind('}')?; + serde_json::from_str::(&candidate[start..=end]).ok() + }) +} + +pub(super) fn extract_content_factory_workspace_patch_from_artifact_document( + metadata: Option<&Value>, +) -> Option { + let metadata = metadata?; + let artifact_document = metadata + .get("artifactDocument") + .or_else(|| metadata.get("artifact_document"))?; + let blocks = artifact_document.get("blocks")?.as_array()?; + + blocks.iter().find_map(|block| { + let text = block + .get("content") + .or_else(|| block.get("markdown")) + .and_then(Value::as_str)?; + let parsed = parse_json_object_from_markdown(text)?; + extract_content_factory_workspace_patch(Some(&parsed)) + }) +} + +fn build_artifact_event_payload(artifact: &AgentRuntimeThreadArtifactView) -> Option { + let artifact_value = serde_json::to_value(artifact).ok()?; + let workspace_patch = extract_content_factory_workspace_patch(artifact.metadata.as_ref()) + .or_else(|| { + extract_content_factory_workspace_patch_from_artifact_document( + artifact.metadata.as_ref(), + ) + }); + if let Some(workspace_patch) = workspace_patch { + return Some(json!({ + "artifact": artifact_value, + "workspacePatch": workspace_patch, + "contentFactoryWorkspacePatch": workspace_patch, + "producer": "agent_runtime_artifact_metadata", + })); + } + Some(artifact_value) +} + +pub(super) fn build_agent_app_runtime_task_events( + thread_read: &AgentRuntimeThreadReadModel, +) -> Vec { + let mut events = Vec::new(); + + for queued_turn in &thread_read.queued_turns { + push_task_event( + &mut events, + "task:queued", + "queued", + queued_turn.message_preview.clone(), + None, + serde_json::to_value(queued_turn).ok(), + ); + } + + let status_message = match thread_read.profile_status.as_str() { + "idle" => "任务已接收,等待 AgentRuntime 调度或回写进度".to_string(), + "queued" => "任务已进入队列".to_string(), + "running" => "任务正在执行".to_string(), + "blocked" => "任务等待用户或权限响应".to_string(), + "completed" => "任务已完成".to_string(), + "failed" => "任务执行失败".to_string(), + "cancelled" => "任务已取消".to_string(), + _ => format!("任务状态:{}", thread_read.status), + }; + push_task_event( + &mut events, + "task:progress", + thread_read.profile_status.as_str(), + status_message, + thread_read.updated_at.clone(), + Some(json!({ + "thread_id": thread_read.thread_id.clone(), + "active_turn_id": thread_read.active_turn_id.clone(), + "profile_status": thread_read.profile_status.clone(), + "status": thread_read.status.clone(), + })), + ); + + for pending_request in &thread_read.pending_requests { + let message = pending_request + .title + .clone() + .unwrap_or_else(|| "任务等待 Host / 用户响应".to_string()); + let mut event = AgentAppRuntimeTaskEvent { + id: format!("task:reviewRequested:{}", pending_request.id), + event_type: "task:reviewRequested".to_string(), + status: pending_request.status.clone(), + message, + severity: None, + turn_id: pending_request.turn_id.clone(), + request_id: Some(pending_request.id.clone()), + tool_name: None, + evidence_ref: None, + artifact_ref: None, + occurred_at: pending_request.created_at.clone(), + payload: serde_json::to_value(pending_request).ok(), + }; + if matches!( + pending_request.request_type.as_str(), + "missing_context" | "ask_user" | "elicitation" + ) { + event.event_type = "task:missingContextRequested".to_string(); + } + events.push(event); + } + + if let Some(missing_context) = has_missing_context(thread_read.context_summary.as_ref()) { + push_task_event( + &mut events, + "task:missingContextRequested", + "blocked", + "任务需要补齐上下文", + thread_read.updated_at.clone(), + Some(json!({ "missing_context": missing_context })), + ); + } + + for tool_call in &thread_read.tool_calls { + events.push(AgentAppRuntimeTaskEvent { + id: format!("task:toolCall:{}", tool_call.tool_call_id), + event_type: "task:toolCall".to_string(), + status: tool_call.status.clone(), + message: format!("工具 {} {}", tool_call.tool_name, tool_call.status), + severity: if tool_call.success == Some(false) { + Some("warning".to_string()) + } else { + None + }, + turn_id: Some(tool_call.turn_id.clone()), + request_id: None, + tool_name: Some(tool_call.tool_name.clone()), + evidence_ref: None, + artifact_ref: None, + occurred_at: None, + payload: serde_json::to_value(tool_call).ok(), + }); + } + + for artifact in &thread_read.artifacts { + let payload = build_artifact_event_payload(artifact); + let workspace_patch = payload.as_ref().and_then(|value| { + value + .get("contentFactoryWorkspacePatch") + .or_else(|| value.get("workspacePatch")) + .cloned() + }); + events.push(AgentAppRuntimeTaskEvent { + id: format!("artifact:created:{}", artifact.item_id), + event_type: "artifact:created".to_string(), + status: artifact.status.clone(), + message: artifact + .title + .clone() + .unwrap_or_else(|| format!("Artifact 已创建:{}", artifact.path)), + severity: (artifact.status == "failed").then(|| "error".to_string()), + turn_id: Some(artifact.turn_id.clone()), + request_id: None, + tool_name: None, + evidence_ref: None, + artifact_ref: Some(artifact.path.clone()), + occurred_at: artifact + .completed_at + .clone() + .or_else(|| artifact.updated_at.clone()) + .or_else(|| artifact.created_at.clone()), + payload, + }); + if let Some(workspace_patch) = workspace_patch { + let evidence_ref = format!("evidence:{}", artifact.path); + events.push(AgentAppRuntimeTaskEvent { + id: format!("evidence:recorded:{}", artifact.item_id), + event_type: "evidence:recorded".to_string(), + status: "recorded".to_string(), + message: "内容工厂 workspace patch evidence 已记录".to_string(), + severity: None, + turn_id: Some(artifact.turn_id.clone()), + request_id: None, + tool_name: None, + evidence_ref: Some(evidence_ref.clone()), + artifact_ref: Some(artifact.path.clone()), + occurred_at: artifact + .completed_at + .clone() + .or_else(|| artifact.updated_at.clone()) + .or_else(|| artifact.created_at.clone()), + payload: Some(json!({ + "artifactRef": artifact.path.clone(), + "evidenceRef": evidence_ref, + "workspacePatch": workspace_patch.clone(), + "contentFactoryWorkspacePatch": workspace_patch, + "source": "agent_runtime_artifact_replay", + })), + }); + } + } + + for evidence_ref in &thread_read.evidence_summary.evidence_refs { + events.push(AgentAppRuntimeTaskEvent { + id: format!("evidence:recorded:{evidence_ref}"), + event_type: "evidence:recorded".to_string(), + status: "recorded".to_string(), + message: "运行证据已记录".to_string(), + severity: None, + turn_id: None, + request_id: None, + tool_name: None, + evidence_ref: Some(evidence_ref.clone()), + artifact_ref: None, + occurred_at: thread_read.updated_at.clone(), + payload: None, + }); + } + + for (index, outcome) in thread_read + .evidence_summary + .verification_outcomes + .iter() + .enumerate() + { + push_task_event( + &mut events, + "evidence:verified", + "verified", + "运行证据已验证", + thread_read.updated_at.clone(), + Some(json!({ "index": index, "outcome": outcome })), + ); + } + + if let Some(outcome) = &thread_read.last_outcome { + let event_type = outcome_event_type(&outcome.outcome_type); + events.push(AgentAppRuntimeTaskEvent { + id: format!( + "{}:{}", + event_type, + outcome.turn_id.as_deref().unwrap_or("latest") + ), + event_type: event_type.to_string(), + status: outcome.outcome_type.clone(), + message: outcome + .summary + .clone() + .or_else(|| outcome.primary_cause.clone()) + .unwrap_or_else(|| "任务回合已结束".to_string()), + severity: (event_type == "task:error").then(|| "error".to_string()), + turn_id: outcome.turn_id.clone(), + request_id: None, + tool_name: None, + evidence_ref: None, + artifact_ref: None, + occurred_at: outcome.ended_at.clone(), + payload: serde_json::to_value(outcome).ok(), + }); + } + + for incident in &thread_read.incidents { + events.push(AgentAppRuntimeTaskEvent { + id: format!("task:incident:{}", incident.id), + event_type: "task:incident".to_string(), + status: incident.status.clone(), + message: incident.title.clone(), + severity: Some(incident.severity.clone()), + turn_id: incident.turn_id.clone(), + request_id: None, + tool_name: None, + evidence_ref: None, + artifact_ref: None, + occurred_at: incident.detected_at.clone(), + payload: serde_json::to_value(incident).ok(), + }); + } + + events +} + +pub(super) fn build_agent_app_runtime_task_snapshot_event_payload( + snapshot: &AgentAppRuntimeTaskSnapshot, +) -> Value { + let snapshot_value = serde_json::to_value(snapshot).unwrap_or_else(|_| json!({})); + json!({ + "type": "agent_app_runtime:taskSnapshot", + "eventType": "task:update", + "appId": snapshot.app_id.clone(), + "taskId": snapshot.task_id.clone(), + "sessionId": snapshot.session_id.clone(), + "taskStatus": snapshot.task_status.clone(), + "status": snapshot.status.clone(), + "task": snapshot_value.clone(), + "snapshot": snapshot_value, + "taskEvents": snapshot.task_events.clone(), + "threadRead": snapshot.thread_read.clone(), + "emittedAt": Utc::now().to_rfc3339(), + }) +} + +pub(super) fn emit_agent_app_runtime_task_snapshot( + app: &AppHandle, + snapshot: &AgentAppRuntimeTaskSnapshot, +) { + let event_name = agent_app_runtime_event_name(&snapshot.app_id, &snapshot.task_id); + let payload = build_agent_app_runtime_task_snapshot_event_payload(snapshot); + if let Err(error) = app.emit(&event_name, payload) { + tracing::warn!( + "[AgentAppRuntime] 发送 App task projection event 失败: event_name={}, error={}", + event_name, + error + ); + } +} diff --git a/src-tauri/src/commands/agent_app_runtime_cmd/metadata.rs b/src-tauri/src/commands/agent_app_runtime_cmd/metadata.rs new file mode 100644 index 000000000..6b079ac44 --- /dev/null +++ b/src-tauri/src/commands/agent_app_runtime_cmd/metadata.rs @@ -0,0 +1,721 @@ +use super::types::AgentAppRuntimeStartTaskRequest; +use super::{ + non_empty, AGENT_APP_RUNTIME_CAPABILITY_SOURCE, AGENT_APP_RUNTIME_METADATA_KEY, + CONTENT_FACTORY_WORKSPACE_PATCH_KIND, LIME_RUNTIME_METADATA_KEY, LIME_RUNTIME_TOOL_SURFACE_KEY, +}; +use crate::services::agent_app_runtime_capability_catalog_service::{ + resolve_capability_descriptors, AgentAppRuntimeCapabilityDescriptor, +}; +use serde_json::{json, Map, Value}; +use std::collections::HashSet; + +fn default_task_message(request: &AgentAppRuntimeStartTaskRequest) -> String { + let title = non_empty(request.title.as_deref()) + .or_else(|| non_empty(request.prompt.as_deref())) + .unwrap_or_else(|| request.task_kind.trim().to_string()); + let input = request + .input + .as_ref() + .map(|value| serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())) + .unwrap_or_else(|| "{}".to_string()); + let expected_output = request + .expected_output + .as_ref() + .map(|value| serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())) + .unwrap_or_else(|| "{}".to_string()); + + [ + "【Agent App Runtime Task】".to_string(), + format!("App: {}", request.app_id.trim()), + format!( + "Entry: {}", + request.entry_key.as_deref().unwrap_or("default").trim() + ), + format!("TaskKind: {}", request.task_kind.trim()), + format!("Title: {title}"), + "".to_string(), + "请在 Lime AgentRuntime 主链中完成这个 App 业务任务。".to_string(), + "不要要求用户跳回通用 Chat;如需补充上下文,请通过可审计的 action / request 机制表达。" + .to_string(), + "".to_string(), + "Input JSON:".to_string(), + input, + "".to_string(), + "Expected Output JSON:".to_string(), + expected_output, + ] + .join("\n") +} + +fn expected_artifact_kind(request: &AgentAppRuntimeStartTaskRequest) -> Option { + let expected_output = request.expected_output.as_ref()?.as_object()?; + [ + "artifactKind", + "artifact_type", + "artifactType", + "kind", + "outputKind", + ] + .iter() + .filter_map(|key| expected_output.get(*key).and_then(Value::as_str)) + .find_map(|value| non_empty(Some(value))) +} + +fn is_content_factory_runtime_task(request: &AgentAppRuntimeStartTaskRequest) -> bool { + request.app_id.trim() == "content-factory-app" + || request.task_kind.trim().starts_with("content_factory.") +} + +fn build_agent_app_output_contract(request: &AgentAppRuntimeStartTaskRequest) -> Option { + if !is_content_factory_runtime_task(request) { + return None; + } + let artifact_kind = expected_artifact_kind(request)?; + Some(json!({ + "producer": "agent_runtime_artifact_metadata", + "artifact_kind": artifact_kind, + "artifact_metadata_kind": CONTENT_FACTORY_WORKSPACE_PATCH_KIND, + "patch_metadata_keys": ["contentFactoryWorkspacePatch", "workspacePatch"], + "required_patch_fields": ["kind", "projectId"], + "accepted_patch_fields": [ + "workspace", + "project", + "sceneTable", + "contentBatch", + "scripts", + "imagePrompts", + "assetPack" + ], + })) +} + +fn read_object_string(object: &Map, keys: &[&str]) -> Option { + keys.iter().find_map(|key| { + object + .get(*key) + .and_then(Value::as_str) + .and_then(|value| non_empty(Some(value))) + }) +} + +fn normalize_agent_app_skill_ref(value: &Value) -> Option { + if let Some(skill) = value.as_str().and_then(|value| non_empty(Some(value))) { + return Some(json!({ + "id": skill, + "skill": skill, + "standard": "agentskills", + "required": true, + })); + } + + let object = value.as_object()?; + let id = read_object_string(object, &["id", "skillId", "skill_id", "skill", "name"])?; + let skill = read_object_string(object, &["skill", "skillName", "skill_name"]) + .unwrap_or_else(|| id.clone()); + let standard = + read_object_string(object, &["standard"]).unwrap_or_else(|| "agentskills".to_string()); + let required = object + .get("required") + .and_then(Value::as_bool) + .unwrap_or(true); + + let mut normalized = Map::new(); + normalized.insert("id".to_string(), json!(id)); + normalized.insert("skill".to_string(), json!(skill)); + normalized.insert("standard".to_string(), json!(standard)); + normalized.insert("required".to_string(), json!(required)); + for key in ["title", "role", "description", "reason"] { + if let Some(value) = read_object_string(object, &[key]) { + normalized.insert(key.to_string(), json!(value)); + } + } + + Some(Value::Object(normalized)) +} + +fn skill_ref_key(value: &Value) -> Option { + let object = value.as_object()?; + let standard = object + .get("standard") + .and_then(Value::as_str) + .unwrap_or("agentskills"); + let skill = object + .get("skill") + .or_else(|| object.get("id")) + .and_then(Value::as_str)?; + Some(format!( + "{}:{}", + standard.trim().to_ascii_lowercase(), + skill.trim().to_ascii_lowercase() + )) +} + +fn collect_agent_app_skill_refs_from_array( + value: Option<&Value>, + refs: &mut Vec, + seen: &mut HashSet, +) { + let Some(items) = value.and_then(Value::as_array) else { + return; + }; + for item in items { + let Some(normalized) = normalize_agent_app_skill_ref(item) else { + continue; + }; + let Some(key) = skill_ref_key(&normalized) else { + continue; + }; + if seen.insert(key) { + refs.push(normalized); + } + } +} + +fn collect_agent_app_skill_refs_from_object( + object: &Map, + refs: &mut Vec, + seen: &mut HashSet, +) { + for key in [ + "requiredSkills", + "required_skills", + "skills", + "skillRefs", + "skill_refs", + ] { + collect_agent_app_skill_refs_from_array(object.get(key), refs, seen); + } + + for key in [ + "agentTaskContract", + "agent_task_contract", + "contentFactory", + "content_factory", + "skillContract", + "skill_contract", + ] { + if let Some(nested) = object.get(key).and_then(Value::as_object) { + collect_agent_app_skill_refs_from_object(nested, refs, seen); + } + } +} + +fn collect_agent_app_skill_refs_from_value( + value: Option<&Value>, + refs: &mut Vec, + seen: &mut HashSet, +) { + if let Some(object) = value.and_then(Value::as_object) { + collect_agent_app_skill_refs_from_object(object, refs, seen); + } +} + +fn collect_agent_app_skill_refs(request: &AgentAppRuntimeStartTaskRequest) -> Vec { + let mut refs = Vec::new(); + let mut seen = HashSet::new(); + collect_agent_app_skill_refs_from_value(request.input.as_ref(), &mut refs, &mut seen); + collect_agent_app_skill_refs_from_value(request.expected_output.as_ref(), &mut refs, &mut seen); + collect_agent_app_skill_refs_from_value(request.metadata.as_ref(), &mut refs, &mut seen); + refs +} + +fn build_agent_app_skill_contract(request: &AgentAppRuntimeStartTaskRequest) -> Option { + if !is_content_factory_runtime_task(request) { + return None; + } + let required_skills = collect_agent_app_skill_refs(request); + if required_skills.is_empty() { + return None; + } + + Some(json!({ + "source": AGENT_APP_RUNTIME_CAPABILITY_SOURCE, + "app_id": request.app_id.trim(), + "task_kind": request.task_kind.trim(), + "standard": "agentskills", + "invocation": "Lime AgentRuntime Skill tool", + "policy": "must_use_required_skills_before_final_patch", + "required_skills": required_skills, + "evidence_required": true, + "evidence_fields": ["skillId", "skill", "status", "summary"], + })) +} + +fn render_agent_app_skill_contract_lines(skill_contract: &Value) -> Vec { + let skills = skill_contract + .get("required_skills") + .and_then(Value::as_array) + .map(|items| { + items + .iter() + .filter_map(Value::as_object) + .filter_map(|skill| { + let skill_name = + read_object_string(skill, &["skill", "id"]).unwrap_or_default(); + if skill_name.is_empty() { + return None; + } + let title = read_object_string(skill, &["title", "role", "description"]); + Some(match title { + Some(title) => format!("- skill=\"{skill_name}\":{title}"), + None => format!("- skill=\"{skill_name}\""), + }) + }) + .collect::>() + }) + .unwrap_or_default(); + + let mut lines = vec![ + "".to_string(), + "Content Factory Skill Contract:".to_string(), + "- 本任务必须通过 Lime AgentRuntime 的 Skill 工具调用下列业务 Skills;不得绕过 Skill 直接普通回答。".to_string(), + "- 本回合只应调用工具名 Skill;不要调用 Agent、Bash、Write、Read、Edit、Glob 或 Grep 来模拟业务 Skill。".to_string(), + "- 调用示例:tool=Skill, args={\"skill\":\"knowledge-builder\",\"args\":\"基于 Input JSON 整理项目知识库\"}。".to_string(), + "- 参数 skill 必须使用下面的 skill 值;至少调用 required=true 的 Skill。".to_string(), + "- Skill 负责生产工艺,App 负责页面流程和产物回写;最终仍必须产出 contentFactoryWorkspacePatch / workspacePatch。".to_string(), + "- 最终 patch 或 evidence 中必须记录 skillEvidence / skillRefs,说明每个 required Skill 的使用状态。".to_string(), + ]; + lines.extend(skills); + lines +} + +fn insert_agent_app_required_skill_tool_scope(root: &mut Map) { + root.insert( + "tool_scope".to_string(), + json!({ + "source": AGENT_APP_RUNTIME_CAPABILITY_SOURCE, + "reason": "agent_app_required_skills", + "allowed_tools": ["Skill"], + }), + ); + let lime_runtime = root + .entry(LIME_RUNTIME_METADATA_KEY.to_string()) + .or_insert_with(|| json!({})); + if let Some(lime_runtime) = lime_runtime.as_object_mut() { + // 占住 tool_surface,阻止 provider 默认 compact_tools 把 Skill 从工具面过滤掉。 + lime_runtime.insert( + LIME_RUNTIME_TOOL_SURFACE_KEY.to_string(), + json!("agent_app_required_skills"), + ); + } +} + +pub(super) fn build_agent_app_runtime_task_message( + request: &AgentAppRuntimeStartTaskRequest, +) -> String { + let prompt = non_empty(request.prompt.as_deref()) + .or_else(|| non_empty(request.title.as_deref())) + .unwrap_or_else(|| request.task_kind.trim().to_string()); + let input = request + .input + .as_ref() + .map(|value| serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())) + .unwrap_or_else(|| "{}".to_string()); + let expected_output = request + .expected_output + .as_ref() + .map(|value| serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())) + .unwrap_or_else(|| "{}".to_string()); + let mut lines = vec![ + "【Agent App Runtime Task】".to_string(), + format!("App: {}", request.app_id.trim()), + format!( + "Entry: {}", + request.entry_key.as_deref().unwrap_or("default").trim() + ), + format!("TaskKind: {}", request.task_kind.trim()), + "".to_string(), + "Business Prompt:".to_string(), + prompt, + "".to_string(), + "Runtime Boundary:".to_string(), + "- 请在 Lime AgentRuntime 主链中完成这个 App 业务任务。".to_string(), + "- 不要要求用户跳回通用 Chat;如需补充上下文,请通过可审计的 action / request 机制表达。" + .to_string(), + ]; + + if let Some(skill_contract) = build_agent_app_skill_contract(request) { + lines.extend(render_agent_app_skill_contract_lines(&skill_contract)); + } + + if let Some(contract) = build_agent_app_output_contract(request) { + let artifact_kind = contract + .get("artifact_kind") + .and_then(Value::as_str) + .unwrap_or("content_batch"); + lines.extend([ + "".to_string(), + "Content Factory Output Contract:".to_string(), + format!( + "- 如果任务产出可直接物化到内容工厂项目,必须创建 artifactKind={artifact_kind} 的 artifact。" + ), + format!( + "- artifact metadata 必须包含 contentFactoryWorkspacePatch 或 workspacePatch;metadata.kind 可使用 {}。", + CONTENT_FACTORY_WORKSPACE_PATCH_KIND + ), + "- 不要通过 Bash、shell、脚本或直接写 .lime/artifacts 文件来伪造 artifact;最终回答应直接输出结构化 JSON。" + .to_string(), + "- 最终回答的顶层 JSON 必须包含 contentFactoryWorkspacePatch 或 workspacePatch,方便 Host 自动回写当前 App 页面。" + .to_string(), + "- patch 至少包含 kind / projectId,并按结果类型填写 sceneTable、contentBatch、scripts、imagePrompts 或 assetPack。" + .to_string(), + "- tools / capabilityHints 只是可选能力提示,不能把复合内容工厂任务改写成单一 research / image Skill;业务 requiredSkills 必须先通过 Skill 工具执行,最终仍收敛为 workspace patch。" + .to_string(), + "- 不要只返回自然语言总结;结构化 patch 是 App 自动回写当前页面的事实源。".to_string(), + ]); + } + + lines.extend([ + "".to_string(), + "Input JSON:".to_string(), + input, + "".to_string(), + "Expected Output JSON:".to_string(), + expected_output, + ]); + + lines.join("\n") +} + +fn insert_string_if_some(map: &mut Map, key: &str, value: Option) { + if let Some(value) = value.map(|item| item.trim().to_string()) { + if !value.is_empty() { + map.insert(key.to_string(), json!(value)); + } + } +} + +fn app_task_prompt_summary(request: &AgentAppRuntimeStartTaskRequest) -> String { + non_empty(request.prompt.as_deref()) + .or_else(|| non_empty(request.title.as_deref())) + .unwrap_or_else(|| default_task_message(request)) +} + +fn build_agent_app_capability_request_context( + request: &AgentAppRuntimeStartTaskRequest, + task_id: &str, + trace_id: &str, + descriptor: AgentAppRuntimeCapabilityDescriptor, +) -> Map { + let prompt = app_task_prompt_summary(request); + let mut context = Map::new(); + + context.insert( + "source".to_string(), + json!(AGENT_APP_RUNTIME_CAPABILITY_SOURCE), + ); + context.insert("capability_id".to_string(), json!(descriptor.capability_id)); + context.insert("app_id".to_string(), json!(request.app_id.trim())); + context.insert("task_id".to_string(), json!(task_id)); + context.insert("trace_id".to_string(), json!(trace_id)); + context.insert("task_kind".to_string(), json!(request.task_kind.trim())); + context.insert( + "entry_source".to_string(), + json!(AGENT_APP_RUNTIME_CAPABILITY_SOURCE), + ); + context.insert("raw_text".to_string(), json!(prompt.clone())); + context.insert("prompt".to_string(), json!(prompt)); + context.insert( + "required_capabilities".to_string(), + json!(request.required_capabilities.clone()), + ); + context.insert( + "capability_hints".to_string(), + json!(request.capability_hints.clone()), + ); + context.insert( + "human_review".to_string(), + json!(request.human_review.unwrap_or(false)), + ); + + insert_string_if_some( + &mut context, + "entry_key", + non_empty(request.entry_key.as_deref()), + ); + insert_string_if_some( + &mut context, + "workspace_id", + non_empty(request.workspace_id.as_deref()), + ); + insert_string_if_some( + &mut context, + "idempotency_key", + non_empty(request.idempotency_key.as_deref()), + ); + + if let Some(input) = request.input.clone() { + context.insert("input".to_string(), input); + } + if let Some(expected_output) = request.expected_output.clone() { + context.insert("expected_output".to_string(), expected_output); + } + if !request.knowledge_bindings.is_empty() { + context.insert( + "knowledge_bindings".to_string(), + json!(request.knowledge_bindings.clone()), + ); + } + + match descriptor.context_key { + "image_task" | "cover_task" => { + context.insert("mode".to_string(), json!("generate")); + } + "research_request" | "report_request" => { + if !context.contains_key("query") { + let query = context + .get("prompt") + .and_then(Value::as_str) + .unwrap_or("请根据当前 App 任务补齐资料") + .to_string(); + context.insert("query".to_string(), json!(query)); + } + } + _ => {} + } + + context +} + +fn resolve_agent_app_runtime_capability_descriptors( + request: &AgentAppRuntimeStartTaskRequest, +) -> Vec { + resolve_capability_descriptors( + request + .required_capabilities + .iter() + .map(String::as_str) + .chain(request.capability_hints.iter().map(String::as_str)), + ) +} + +fn capability_descriptor_metadata(descriptor: AgentAppRuntimeCapabilityDescriptor) -> Value { + json!({ + "capability_id": descriptor.capability_id, + "skill_name": descriptor.skill_name, + "launch_key": descriptor.launch_key, + "context_key": descriptor.context_key, + "default_kind": descriptor.default_kind, + }) +} + +fn build_agent_app_capability_workflow_metadata( + request: &AgentAppRuntimeStartTaskRequest, + descriptors: &[AgentAppRuntimeCapabilityDescriptor], + output_contract: Option<&Value>, + inserts_primary_launch: bool, +) -> Option { + if descriptors.is_empty() { + return None; + } + + Some(json!({ + "source": AGENT_APP_RUNTIME_CAPABILITY_SOURCE, + "mode": if output_contract.is_some() { + "composite_output_contract" + } else if descriptors.len() > 1 { + "multi_capability" + } else { + "single_capability" + }, + "launch_policy": if inserts_primary_launch { + "primary_skill_launch" + } else { + "metadata_only" + }, + "requested_capabilities": request.required_capabilities.clone(), + "capability_hints": request.capability_hints.clone(), + "descriptors": descriptors + .iter() + .copied() + .map(capability_descriptor_metadata) + .collect::>(), + })) +} + +fn insert_agent_app_capability_launch_metadata( + root: &mut Map, + request: &AgentAppRuntimeStartTaskRequest, + task_id: &str, + trace_id: &str, + descriptor: AgentAppRuntimeCapabilityDescriptor, +) { + let launch_context = + build_agent_app_capability_request_context(request, task_id, trace_id, descriptor); + let harness = root + .entry("harness".to_string()) + .or_insert_with(|| json!({})); + let Some(harness) = harness.as_object_mut() else { + return; + }; + + harness.insert("allow_model_skills".to_string(), json!(true)); + let mut launch = Map::new(); + launch.insert("skill_name".to_string(), json!(descriptor.skill_name)); + launch.insert("kind".to_string(), json!(descriptor.default_kind)); + launch.insert( + descriptor.context_key.to_string(), + Value::Object(launch_context), + ); + harness.insert(descriptor.launch_key.to_string(), Value::Object(launch)); +} + +fn should_insert_agent_app_capability_launch_metadata( + request: &AgentAppRuntimeStartTaskRequest, + output_contract: Option<&Value>, +) -> bool { + // 内容工厂这类复合业务任务的 tools/capabilityHints 表示“可用能力”, + // 不能被提升为单一 Claw Skill 启动,否则会偏离 App 的 workspace patch 产物合同。 + if is_content_factory_runtime_task(request) && output_contract.is_some() { + return false; + } + + true +} + +fn insert_agent_app_output_contract_runtime_hints( + harness: &mut Map, + request: &AgentAppRuntimeStartTaskRequest, + output_contract: Option<&Value>, +) { + if !is_content_factory_runtime_task(request) || output_contract.is_none() { + return; + } + + // 内容工厂 patch 产出优先走直接回答 + ArtifactDocument 自动落盘, + // 避免复合业务任务被 FullRuntime 的通用工具链带偏成读文件 / Bash / 子代理循环。 + harness + .entry("chat_mode".to_string()) + .or_insert_with(|| json!("general")); + harness + .entry("session_mode".to_string()) + .or_insert_with(|| json!("general_workbench")); +} + +pub(super) fn build_agent_app_runtime_metadata( + request: &AgentAppRuntimeStartTaskRequest, + task_id: &str, + trace_id: &str, +) -> Value { + let mut metadata = request.metadata.clone().unwrap_or_else(|| json!({})); + if !metadata.is_object() { + metadata = json!({}); + } + + let mut app_runtime = json!({ + "surface": "agent_app", + "app_id": request.app_id.trim(), + "entry_key": request.entry_key.as_deref().unwrap_or("").trim(), + "task_id": task_id, + "trace_id": trace_id, + "task_kind": request.task_kind.trim(), + "idempotency_key": request.idempotency_key.as_deref().unwrap_or("").trim(), + "required_capabilities": request.required_capabilities.clone(), + "capability_hints": request.capability_hints.clone(), + "knowledge_bindings": request.knowledge_bindings.clone(), + "human_review": request.human_review.unwrap_or(false), + }); + let output_contract = build_agent_app_output_contract(request); + let skill_contract = build_agent_app_skill_contract(request); + let capability_descriptors = resolve_agent_app_runtime_capability_descriptors(request); + let should_insert_primary_capability_launch = + should_insert_agent_app_capability_launch_metadata(request, output_contract.as_ref()) + && !capability_descriptors.is_empty(); + let capability_workflow = build_agent_app_capability_workflow_metadata( + request, + &capability_descriptors, + output_contract.as_ref(), + should_insert_primary_capability_launch, + ); + if let (Some(app_runtime), Some(output_contract)) = + (app_runtime.as_object_mut(), output_contract.clone()) + { + app_runtime.insert("output_contract".to_string(), output_contract); + } + if let (Some(app_runtime), Some(capability_workflow)) = + (app_runtime.as_object_mut(), capability_workflow.clone()) + { + app_runtime.insert("capability_workflow".to_string(), capability_workflow); + } + if let (Some(app_runtime), Some(skill_contract)) = + (app_runtime.as_object_mut(), skill_contract.clone()) + { + app_runtime.insert("skill_contract".to_string(), skill_contract); + } + + if let Some(root) = metadata.as_object_mut() { + root.insert( + AGENT_APP_RUNTIME_METADATA_KEY.to_string(), + app_runtime.clone(), + ); + let lime_runtime = root + .entry(LIME_RUNTIME_METADATA_KEY.to_string()) + .or_insert_with(|| json!({})); + if let Some(lime_runtime) = lime_runtime.as_object_mut() { + lime_runtime.insert("surface".to_string(), json!("agent_app")); + lime_runtime.insert("app_id".to_string(), json!(request.app_id.trim())); + lime_runtime.insert("task_id".to_string(), json!(task_id)); + lime_runtime.insert("trace_id".to_string(), json!(trace_id)); + lime_runtime.insert("task_kind".to_string(), json!(request.task_kind.trim())); + let runtime_summary = lime_runtime + .entry("runtime_summary".to_string()) + .or_insert_with(|| json!({})); + if let Some(runtime_summary) = runtime_summary.as_object_mut() { + runtime_summary.insert("surface".to_string(), json!("agent_app")); + runtime_summary.insert("app_id".to_string(), json!(request.app_id.trim())); + runtime_summary.insert("task_id".to_string(), json!(task_id)); + runtime_summary.insert("trace_id".to_string(), json!(trace_id)); + runtime_summary.insert("task_kind".to_string(), json!(request.task_kind.trim())); + } + } + let should_insert_skill_tool_scope = skill_contract.is_some(); + { + let harness = root + .entry("harness".to_string()) + .or_insert_with(|| json!({})); + if let Some(harness) = harness.as_object_mut() { + harness.insert( + AGENT_APP_RUNTIME_METADATA_KEY.to_string(), + app_runtime.clone(), + ); + if let Some(output_contract) = output_contract.clone() { + harness.insert( + "agent_app_runtime_output_contract".to_string(), + output_contract, + ); + } + insert_agent_app_output_contract_runtime_hints( + harness, + request, + output_contract.as_ref(), + ); + if let Some(skill_contract) = skill_contract.clone() { + harness.insert("allow_model_skills".to_string(), json!(true)); + harness.insert( + "agent_app_runtime_skill_contract".to_string(), + skill_contract.clone(), + ); + harness.insert("content_factory_skill_contract".to_string(), skill_contract); + } + if let Some(capability_workflow) = capability_workflow.clone() { + harness.insert( + "agent_app_runtime_capability_workflow".to_string(), + capability_workflow, + ); + } + } + } + if should_insert_skill_tool_scope { + insert_agent_app_required_skill_tool_scope(root); + } + if should_insert_primary_capability_launch { + if let Some(descriptor) = capability_descriptors.first().copied() { + insert_agent_app_capability_launch_metadata( + root, request, task_id, trace_id, descriptor, + ); + } + } + } + + metadata +} diff --git a/src-tauri/src/commands/agent_app_runtime_cmd/model_preference.rs b/src-tauri/src/commands/agent_app_runtime_cmd/model_preference.rs new file mode 100644 index 000000000..8a96060b8 --- /dev/null +++ b/src-tauri/src/commands/agent_app_runtime_cmd/model_preference.rs @@ -0,0 +1,249 @@ +use super::types::AgentAppRuntimeStartTaskRequest; +use super::{non_empty, AGENT_APP_RUNTIME_METADATA_KEY}; +use crate::agent::AsterAgentState; +use crate::commands::api_key_provider_cmd::ApiKeyProviderServiceState; +use crate::database::DbConnection; +use lime_core::database::dao::agent_run::{AgentRun, AgentRunDao, AgentRunStatus}; +use lime_core::database::dao::api_key_provider::ProviderWithKeys; +use serde_json::{json, Value}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct AgentAppRuntimeModelPreference { + pub(super) provider_preference: String, + pub(super) model_preference: String, + pub(super) source: &'static str, +} + +fn is_unconfigured_model_preference(value: &str) -> bool { + matches!( + value.trim().to_ascii_lowercase().as_str(), + "" | "unconfigured" | "unknown" | "none" | "null" + ) +} + +fn model_preference_from_values( + provider_preference: Option, + model_preference: Option, + source: &'static str, +) -> Option { + let provider_preference = provider_preference + .and_then(|value| non_empty(Some(value.as_str()))) + .filter(|value| !is_unconfigured_model_preference(value))?; + let model_preference = model_preference + .and_then(|value| non_empty(Some(value.as_str()))) + .filter(|value| !is_unconfigured_model_preference(value))?; + + Some(AgentAppRuntimeModelPreference { + provider_preference, + model_preference, + source, + }) +} + +fn json_pointer_string(value: &Value, pointers: &[&str]) -> Option { + pointers.iter().find_map(|pointer| { + value + .pointer(pointer) + .and_then(Value::as_str) + .and_then(|value| non_empty(Some(value))) + }) +} + +pub(super) fn model_preference_from_run_metadata( + metadata: &Value, +) -> Option { + let provider_preference = json_pointer_string( + metadata, + &[ + "/turn_input/provider_routing/provider_selector", + "/turnInput/providerRouting/providerSelector", + "/request_metadata/lime_runtime/routing_decision/selected_provider", + "/request_metadata/lime_runtime/routing_decision/selectedProvider", + "/requestMetadata/limeRuntime/routingDecision/selectedProvider", + ], + ); + let model_preference = json_pointer_string( + metadata, + &[ + "/turn_input/provider_routing/model_name", + "/turnInput/providerRouting/modelName", + "/request_metadata/lime_runtime/routing_decision/selected_model", + "/request_metadata/lime_runtime/routing_decision/selectedModel", + "/requestMetadata/limeRuntime/routingDecision/selectedModel", + ], + ); + + model_preference_from_values( + provider_preference, + model_preference, + "recent_successful_agent_run", + ) +} + +fn model_preference_from_recent_successful_runs( + db: &DbConnection, +) -> Option { + let runs = { + let conn = match db.lock() { + Ok(conn) => conn, + Err(error) => { + tracing::warn!( + "[AgentAppRuntime] 读取最近模型偏好时数据库锁定失败: {}", + error + ); + return None; + } + }; + match AgentRunDao::list_runs(&conn, 50, 0) { + Ok(runs) => runs, + Err(error) => { + tracing::warn!( + "[AgentAppRuntime] 读取最近 agent_runs 失败,跳过模型偏好回填: {}", + error + ); + return None; + } + } + }; + + runs.iter() + .filter(|run| matches!(run.status, AgentRunStatus::Success)) + .find_map(model_preference_from_agent_run) +} + +fn model_preference_from_agent_run(run: &AgentRun) -> Option { + let metadata = run.metadata.as_deref()?; + let metadata: Value = serde_json::from_str(metadata).ok()?; + model_preference_from_run_metadata(&metadata) +} + +fn provider_looks_non_chat_agent_runtime_candidate(provider: &ProviderWithKeys) -> bool { + let text = [ + provider.provider.id.as_str(), + provider.provider.name.as_str(), + provider.provider.api_host.as_str(), + ] + .join(" ") + .to_ascii_lowercase(); + + text.contains("fal") + || text.contains("codex") + || text.contains("coding") + || text.contains("gpt-image") + || text.contains("gpt_images") +} + +fn model_preference_from_enabled_provider_catalog( + db: &DbConnection, + api_key_provider_service: &ApiKeyProviderServiceState, +) -> Option { + let providers = match api_key_provider_service.0.get_all_providers(db) { + Ok(providers) => providers, + Err(error) => { + tracing::warn!( + "[AgentAppRuntime] 读取 API Key Providers 失败,跳过模型偏好回填: {}", + error + ); + return None; + } + }; + + providers.into_iter().find_map(|provider| { + if !provider.provider.enabled { + return None; + } + if provider_looks_non_chat_agent_runtime_candidate(&provider) { + return None; + } + if !provider.api_keys.iter().any(|key| key.enabled) { + return None; + } + let model = provider + .provider + .custom_models + .iter() + .find_map(|model| non_empty(Some(model.as_str())))?; + model_preference_from_values( + Some(provider.provider.id), + Some(model), + "enabled_provider_custom_model", + ) + }) +} + +pub(super) async fn resolve_agent_app_runtime_model_preference( + state: &AsterAgentState, + db: &DbConnection, + api_key_provider_service: &ApiKeyProviderServiceState, + request: &AgentAppRuntimeStartTaskRequest, +) -> Option { + if let Some(preference) = model_preference_from_values( + request.provider_preference.clone(), + request.model_preference.clone(), + "request", + ) { + return Some(preference); + } + + if let Some(preference) = model_preference_from_recent_successful_runs(db) { + return Some(preference); + } + + if let Some(preference) = + model_preference_from_enabled_provider_catalog(db, api_key_provider_service) + { + return Some(preference); + } + + if let Some(config) = state.get_provider_config().await { + if let Some(preference) = model_preference_from_values( + config + .provider_selector + .clone() + .or_else(|| Some(config.provider_name.clone())), + Some(config.model_name.clone()), + "current_agent_state", + ) { + return Some(preference); + } + } + + None +} + +pub(super) fn insert_agent_app_runtime_model_preference_metadata( + metadata: &mut Value, + preference: &AgentAppRuntimeModelPreference, +) { + let Some(root) = metadata.as_object_mut() else { + return; + }; + let preference_value = json!({ + "provider_preference": preference.provider_preference.clone(), + "model_preference": preference.model_preference.clone(), + "source": preference.source, + }); + + let harness = root + .entry("harness".to_string()) + .or_insert_with(|| json!({})); + if let Some(harness) = harness.as_object_mut() { + harness.insert( + "agent_app_runtime_model_preference".to_string(), + preference_value.clone(), + ); + if let Some(app_runtime) = harness + .get_mut(AGENT_APP_RUNTIME_METADATA_KEY) + .and_then(Value::as_object_mut) + { + app_runtime.insert("model_preference".to_string(), preference_value.clone()); + } + } + + if let Some(app_runtime) = root + .get_mut(AGENT_APP_RUNTIME_METADATA_KEY) + .and_then(Value::as_object_mut) + { + app_runtime.insert("model_preference".to_string(), preference_value); + } +} diff --git a/src-tauri/src/commands/agent_app_runtime_cmd/tests.rs b/src-tauri/src/commands/agent_app_runtime_cmd/tests.rs new file mode 100644 index 000000000..17f48decf --- /dev/null +++ b/src-tauri/src/commands/agent_app_runtime_cmd/tests.rs @@ -0,0 +1,552 @@ +use super::*; +use crate::agent::QueuedTurnSnapshot; +use crate::commands::aster_agent_cmd::AgentRuntimeThreadReadModel; +use crate::commands::aster_agent_cmd::{ + AgentRuntimeIncidentView, AgentRuntimeOutcomeView, AgentRuntimeRequestView, + AgentRuntimeThreadArtifactView, AgentRuntimeThreadEvidenceSummary, + AgentRuntimeThreadTelemetrySummary, AgentRuntimeThreadToolCallView, +}; +use serde_json::{json, Value}; + +fn runtime_request( + required_capabilities: Vec<&str>, + capability_hints: Vec<&str>, +) -> AgentAppRuntimeStartTaskRequest { + AgentAppRuntimeStartTaskRequest { + app_id: "content-factory-app".to_string(), + entry_key: Some("content_factory".to_string()), + workspace_id: Some("workspace-1".to_string()), + session_id: None, + task_id: None, + task_kind: "content_factory.copy.generate".to_string(), + idempotency_key: None, + title: Some("生成小红书种草文案".to_string()), + prompt: Some("围绕春季护肤新品生成文案,并补齐资料来源".to_string()), + input: Some(json!({ + "platform": "xiaohongshu", + "audience": "敏感肌用户" + })), + expected_output: Some(json!({ + "artifacts": ["copy", "assetBrief"] + })), + required_capabilities: required_capabilities + .into_iter() + .map(str::to_string) + .collect(), + capability_hints: capability_hints.into_iter().map(str::to_string).collect(), + knowledge_bindings: Vec::new(), + human_review: Some(true), + event_name: None, + turn_id: None, + provider_preference: None, + model_preference: None, + queue_if_busy: None, + skip_pre_submit_resume: None, + run_start_hooks: None, + metadata: None, + } +} + +fn base_thread_read() -> AgentRuntimeThreadReadModel { + AgentRuntimeThreadReadModel { + thread_id: "thread-1".to_string(), + status: "running".to_string(), + profile_status: "running".to_string(), + active_turn_id: Some("turn-1".to_string()), + turns: Vec::new(), + pending_requests: Vec::new(), + last_outcome: None, + incidents: Vec::new(), + queued_turns: Vec::new(), + tool_calls: Vec::new(), + artifacts: Vec::new(), + model_routing: None, + evidence_summary: AgentRuntimeThreadEvidenceSummary::default(), + telemetry_summary: AgentRuntimeThreadTelemetrySummary::default(), + context_summary: None, + interrupt_state: None, + updated_at: Some("2026-05-16T00:00:00.000Z".to_string()), + latest_compaction_boundary: None, + file_checkpoint_summary: None, + diagnostics: None, + task_kind: None, + service_model_slot: None, + routing_mode: None, + decision_source: None, + decision_reason: None, + candidate_count: None, + fallback_chain: None, + capability_gap: None, + estimated_cost_class: None, + single_candidate_only: None, + oem_policy: None, + runtime_summary: None, + auxiliary_task_runtime: None, + limit_state: None, + cost_state: None, + permission_state: None, + limit_event: None, + } +} + +#[test] +fn test_agent_app_runtime_session_id_uses_hidden_prefix() { + assert!(new_agent_app_runtime_session_id().starts_with(AGENT_APP_RUNTIME_SESSION_ID_PREFIX)); +} + +#[test] +fn test_agent_app_runtime_model_preference_reads_recent_successful_routing_metadata() { + let metadata = json!({ + "request_metadata": { + "lime_runtime": { + "routing_decision": { + "selected_provider": "deepseek", + "selected_model": "deepseek-v4-flash" + } + } + } + }); + + let preference = model_preference_from_run_metadata(&metadata).expect("recent run preference"); + + assert_eq!(preference.provider_preference, "deepseek"); + assert_eq!(preference.model_preference, "deepseek-v4-flash"); + assert_eq!(preference.source, "recent_successful_agent_run"); +} + +#[test] +fn test_agent_app_runtime_metadata_maps_research_capability_to_claw_launch() { + let request = runtime_request( + vec!["text_generation", "lime.capability.research.search"], + Vec::new(), + ); + let metadata = build_agent_app_runtime_metadata(&request, "task-1", "trace-1"); + let harness = metadata + .get("harness") + .and_then(Value::as_object) + .expect("harness metadata"); + let launch = harness + .get("research_skill_launch") + .and_then(Value::as_object) + .expect("research launch"); + let research_request = launch + .get("research_request") + .and_then(Value::as_object) + .expect("research request"); + let runtime_summary = metadata + .get("lime_runtime") + .and_then(|value| value.get("runtime_summary")) + .and_then(Value::as_object) + .expect("agent app runtime summary"); + + assert_eq!(harness.get("allow_model_skills"), Some(&json!(true))); + assert!(harness.get("agent_app_runtime").is_some()); + assert_eq!(runtime_summary.get("surface"), Some(&json!("agent_app"))); + assert_eq!( + runtime_summary.get("app_id"), + Some(&json!("content-factory-app")) + ); + assert_eq!(runtime_summary.get("task_id"), Some(&json!("task-1"))); + assert_eq!(runtime_summary.get("trace_id"), Some(&json!("trace-1"))); + assert_eq!(launch.get("skill_name"), Some(&json!("research"))); + assert_eq!(launch.get("kind"), Some(&json!("research_request"))); + assert_eq!( + research_request.get("source"), + Some(&json!("agent_app_runtime")) + ); + assert_eq!( + research_request.get("app_id"), + Some(&json!("content-factory-app")) + ); + assert_eq!( + research_request.get("capability_id"), + Some(&json!("lime.capability.research.search")) + ); + assert!(research_request.get("query").is_some()); +} + +#[test] +fn test_agent_app_runtime_metadata_maps_image_alias_to_claw_launch() { + let request = runtime_request(Vec::new(), vec!["image_generation"]); + let metadata = build_agent_app_runtime_metadata(&request, "task-1", "trace-1"); + let launch = metadata + .get("harness") + .and_then(Value::as_object) + .and_then(|harness| harness.get("image_skill_launch")) + .and_then(Value::as_object) + .expect("image launch"); + let image_task = launch + .get("image_task") + .and_then(Value::as_object) + .expect("image task"); + + assert_eq!(launch.get("skill_name"), Some(&json!("image_generate"))); + assert_eq!(launch.get("kind"), Some(&json!("image_task"))); + assert_eq!(image_task.get("mode"), Some(&json!("generate"))); + assert_eq!( + image_task.get("entry_source"), + Some(&json!("agent_app_runtime")) + ); +} + +#[test] +fn test_agent_app_runtime_metadata_ignores_unknown_capability_without_fake_launch() { + let request = runtime_request(vec!["text_generation"], Vec::new()); + let metadata = build_agent_app_runtime_metadata(&request, "task-1", "trace-1"); + let harness = metadata + .get("harness") + .and_then(Value::as_object) + .expect("harness metadata"); + + assert!(harness.get("agent_app_runtime").is_some()); + assert!(harness.get("allow_model_skills").is_none()); + assert!(harness.get("image_skill_launch").is_none()); + assert!(harness.get("research_skill_launch").is_none()); +} + +#[test] +fn test_agent_app_runtime_content_factory_output_contract_is_machine_readable() { + let mut request = runtime_request(Vec::new(), Vec::new()); + request.expected_output = Some(json!({ + "artifactKind": "content_batch", + "includes": ["copy", "script", "image_brief"], + "requiredSkills": [{ + "id": "article-writer", + "skill": "article-writer", + "standard": "agentskills", + "required": true + }] + })); + + let metadata = build_agent_app_runtime_metadata(&request, "task-1", "trace-1"); + let harness = metadata + .get("harness") + .and_then(Value::as_object) + .expect("harness metadata"); + let output_contract = harness + .get("agent_app_runtime_output_contract") + .and_then(Value::as_object) + .expect("output contract"); + assert_eq!( + output_contract.get("artifact_kind"), + Some(&json!("content_batch")) + ); + assert_eq!( + output_contract.get("artifact_metadata_kind"), + Some(&json!(CONTENT_FACTORY_WORKSPACE_PATCH_KIND)) + ); + assert!(output_contract + .get("patch_metadata_keys") + .and_then(Value::as_array) + .is_some_and(|items| items.contains(&json!("contentFactoryWorkspacePatch")))); + + let message = build_agent_app_runtime_task_message(&request); + assert!(message.contains("Content Factory Output Contract")); + assert!(message.contains("Content Factory Skill Contract")); + assert!(message.contains("skill=\"article-writer\"")); + assert!(message.contains("tool=Skill")); + assert!(message.contains("contentFactoryWorkspacePatch")); + assert!(message.contains("artifactKind=content_batch")); + assert!(message.contains("不要通过 Bash")); + assert!(message.contains("requiredSkills")); +} + +#[test] +fn test_agent_app_runtime_content_factory_output_contract_uses_business_skills_without_single_capability_launch( +) { + let mut request = runtime_request(Vec::new(), vec!["research.search", "image_generation"]); + request.expected_output = Some(json!({ + "artifactKind": "content_batch", + "includes": ["copy", "script", "image_brief"], + "requiredSkills": [ + { + "id": "article-writer", + "skill": "article-writer", + "standard": "agentskills", + "required": true + }, + { + "id": "content-reviewer", + "skill": "content-reviewer", + "standard": "agentskills", + "required": true + } + ] + })); + + let metadata = build_agent_app_runtime_metadata(&request, "task-1", "trace-1"); + let harness = metadata + .get("harness") + .and_then(Value::as_object) + .expect("harness metadata"); + + assert!(harness.get("agent_app_runtime_output_contract").is_some()); + assert_eq!(harness.get("chat_mode"), Some(&json!("general"))); + assert_eq!( + harness.get("session_mode"), + Some(&json!("general_workbench")) + ); + let workflow = harness + .get("agent_app_runtime_capability_workflow") + .and_then(Value::as_object) + .expect("capability workflow"); + assert_eq!( + workflow.get("mode"), + Some(&json!("composite_output_contract")) + ); + assert_eq!(workflow.get("launch_policy"), Some(&json!("metadata_only"))); + let descriptors = workflow + .get("descriptors") + .and_then(Value::as_array) + .expect("workflow descriptors"); + assert_eq!(descriptors.len(), 2); + assert!(descriptors + .iter() + .any(|descriptor| descriptor.get("capability_id") + == Some(&json!("lime.capability.research.search")))); + assert!(descriptors + .iter() + .any(|descriptor| descriptor.get("capability_id") + == Some(&json!("lime.capability.image.generate")))); + assert_eq!(harness.get("allow_model_skills"), Some(&json!(true))); + let skill_contract = harness + .get("agent_app_runtime_skill_contract") + .and_then(Value::as_object) + .expect("skill contract"); + assert_eq!( + skill_contract.get("policy"), + Some(&json!("must_use_required_skills_before_final_patch")) + ); + let required_skills = skill_contract + .get("required_skills") + .and_then(Value::as_array) + .expect("required skills"); + assert_eq!(required_skills.len(), 2); + assert!(required_skills + .iter() + .any(|skill| skill.get("skill") == Some(&json!("article-writer")))); + assert!(required_skills + .iter() + .any(|skill| skill.get("skill") == Some(&json!("content-reviewer")))); + let tool_scope = metadata + .get("tool_scope") + .and_then(Value::as_object) + .expect("required skill task should narrow tool scope"); + assert_eq!(tool_scope.get("allowed_tools"), Some(&json!(["Skill"]))); + assert_eq!( + metadata + .get(LIME_RUNTIME_METADATA_KEY) + .and_then(|value| value.get(LIME_RUNTIME_TOOL_SURFACE_KEY)), + Some(&json!("agent_app_required_skills")) + ); + assert!(harness.get("research_skill_launch").is_none()); + assert!(harness.get("image_skill_launch").is_none()); +} + +#[test] +fn test_agent_app_runtime_extracts_workspace_patch_from_artifact_document_blocks() { + let metadata = json!({ + "artifactDocument": { + "blocks": [ + { + "type": "rich_text", + "content": "```json\n{\"contentFactoryWorkspacePatch\":{\"kind\":\"content_batch\",\"projectId\":\"project-1\",\"contentBatch\":{\"items\":[{\"title\":\"示例文案\"}]}}}\n```" + } + ] + } + }); + + let patch = extract_content_factory_workspace_patch_from_artifact_document(Some(&metadata)) + .expect("workspace patch"); + + assert_eq!(patch.get("kind"), Some(&json!("content_batch"))); + assert_eq!(patch.get("projectId"), Some(&json!("project-1"))); + assert!(patch.get("contentBatch").is_some()); +} + +#[test] +fn test_agent_app_runtime_task_events_project_thread_read_facts() { + let mut thread_read = base_thread_read(); + thread_read.queued_turns = vec![QueuedTurnSnapshot { + queued_turn_id: "queued-1".to_string(), + message_preview: "排队任务".to_string(), + message_text: "排队任务完整文本".to_string(), + created_at: 1_789_000_000, + image_count: 0, + position: 0, + }]; + thread_read.pending_requests = vec![AgentRuntimeRequestView { + id: "request-1".to_string(), + thread_id: "thread-1".to_string(), + turn_id: Some("turn-1".to_string()), + item_id: None, + request_type: "ask_user".to_string(), + status: "pending".to_string(), + title: Some("需要确认素材方向".to_string()), + payload: Some(json!({ "question": "是否继续?" })), + decision: None, + scope: None, + created_at: Some("2026-05-16T00:00:01.000Z".to_string()), + resolved_at: None, + }]; + thread_read.context_summary = Some(json!({ + "missing_context": [{ "field": "target_audience" }] + })); + thread_read.tool_calls = vec![AgentRuntimeThreadToolCallView { + tool_call_id: "tool-1".to_string(), + turn_id: "turn-1".to_string(), + tool_name: "Skill(research)".to_string(), + status: "completed".to_string(), + success: Some(true), + error: None, + }]; + thread_read.artifacts = vec![AgentRuntimeThreadArtifactView { + item_id: "artifact-item-1".to_string(), + turn_id: "turn-1".to_string(), + path: ".lime/artifacts/content-batch.json".to_string(), + source: "agent_runtime".to_string(), + status: "created".to_string(), + artifact_type: Some("content_batch".to_string()), + title: Some("内容批次".to_string()), + created_at: Some("2026-05-16T00:00:01.500Z".to_string()), + completed_at: Some("2026-05-16T00:00:01.800Z".to_string()), + updated_at: Some("2026-05-16T00:00:01.800Z".to_string()), + metadata: Some(json!({ + "artifactType": "content_batch", + "workspacePatch": { + "kind": "content_batch", + "projectId": "project-1", + "contentBatch": { "count": 20 } + } + })), + }]; + thread_read.evidence_summary = AgentRuntimeThreadEvidenceSummary { + evidence_refs: vec!["evidence-1".to_string()], + verification_outcomes: vec![json!({ "status": "passed" })], + }; + thread_read.last_outcome = Some(AgentRuntimeOutcomeView { + thread_id: "thread-1".to_string(), + turn_id: Some("turn-1".to_string()), + outcome_type: "completed".to_string(), + summary: Some("任务完成".to_string()), + primary_cause: None, + retryable: false, + ended_at: Some("2026-05-16T00:00:02.000Z".to_string()), + }); + thread_read.incidents = vec![AgentRuntimeIncidentView { + id: "incident-1".to_string(), + thread_id: "thread-1".to_string(), + turn_id: Some("turn-1".to_string()), + item_id: None, + incident_type: "provider_warning".to_string(), + severity: "medium".to_string(), + status: "open".to_string(), + title: "Provider warning".to_string(), + details: None, + detected_at: Some("2026-05-16T00:00:03.000Z".to_string()), + cleared_at: None, + }]; + + let events = build_agent_app_runtime_task_events(&thread_read); + let event_types = events + .iter() + .map(|event| event.event_type.as_str()) + .collect::>(); + + assert!(event_types.contains(&"task:queued")); + assert!(event_types.contains(&"task:progress")); + assert!(event_types.contains(&"task:missingContextRequested")); + assert!(event_types.contains(&"task:toolCall")); + assert!(event_types.contains(&"artifact:created")); + assert!(event_types.contains(&"evidence:recorded")); + assert!(event_types.contains(&"evidence:verified")); + assert!(event_types.contains(&"task:completed")); + assert!(event_types.contains(&"task:incident")); + assert!(events + .iter() + .any(|event| event.request_id.as_deref() == Some("request-1"))); + assert!(events + .iter() + .any(|event| event.evidence_ref.as_deref() == Some("evidence-1"))); + assert!(events.iter().any(|event| { + event.event_type == "evidence:recorded" + && event.evidence_ref.as_deref() == Some("evidence:.lime/artifacts/content-batch.json") + && event + .payload + .as_ref() + .and_then(|payload| payload.get("contentFactoryWorkspacePatch")) + .and_then(|patch| patch.get("contentBatch")) + .and_then(|content_batch| content_batch.get("count")) + == Some(&json!(20)) + })); + assert!(events + .iter() + .any(|event| event.artifact_ref.as_deref() == Some(".lime/artifacts/content-batch.json"))); + let artifact_event = events + .iter() + .find(|event| event.event_type == "artifact:created") + .expect("artifact event"); + assert_eq!( + artifact_event + .payload + .as_ref() + .and_then(|payload| payload.get("contentFactoryWorkspacePatch")) + .and_then(|patch| patch.get("contentBatch")) + .and_then(|content_batch| content_batch.get("count")), + Some(&json!(20)) + ); +} + +#[test] +fn test_agent_app_runtime_task_snapshot_event_payload_is_canonical() { + let mut thread_read = base_thread_read(); + thread_read.profile_status = "running".to_string(); + let task_events = build_agent_app_runtime_task_events(&thread_read); + let snapshot = AgentAppRuntimeTaskSnapshot { + app_id: "content-factory-app".to_string(), + task_id: "task-1".to_string(), + session_id: "session-1".to_string(), + status: "thread_read_available".to_string(), + task_status: thread_read.profile_status.clone(), + task_events, + thread_read: serde_json::to_value(&thread_read).expect("thread read value"), + }; + + let payload = build_agent_app_runtime_task_snapshot_event_payload(&snapshot); + + assert_eq!( + payload.get("type"), + Some(&json!("agent_app_runtime:taskSnapshot")) + ); + assert_eq!(payload.get("eventType"), Some(&json!("task:update"))); + assert_eq!(payload.get("taskId"), Some(&json!("task-1"))); + assert_eq!( + payload + .get("taskEvents") + .and_then(Value::as_array) + .map(Vec::len), + Some(snapshot.task_events.len()) + ); + assert!(payload.get("threadRead").is_some()); + assert!(payload.get("task").is_some()); +} + +#[test] +fn test_agent_app_runtime_idle_status_uses_business_progress_copy() { + let mut thread_read = base_thread_read(); + thread_read.status = "idle".to_string(); + thread_read.profile_status = "idle".to_string(); + thread_read.active_turn_id = None; + + let events = build_agent_app_runtime_task_events(&thread_read); + let progress = events + .iter() + .find(|event| event.event_type == "task:progress") + .expect("progress event"); + + assert_eq!( + progress.message, + "任务已接收,等待 AgentRuntime 调度或回写进度" + ); + assert_ne!(progress.message, "任务状态:idle"); +} diff --git a/src-tauri/src/commands/agent_app_runtime_cmd/types.rs b/src-tauri/src/commands/agent_app_runtime_cmd/types.rs new file mode 100644 index 000000000..8d685a35b --- /dev/null +++ b/src-tauri/src/commands/agent_app_runtime_cmd/types.rs @@ -0,0 +1,149 @@ +use crate::commands::aster_agent_cmd::AgentRuntimeRespondActionRequest; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentAppRuntimeStartTaskRequest { + pub app_id: String, + #[serde(default)] + pub entry_key: Option, + #[serde(default)] + pub workspace_id: Option, + #[serde(default)] + pub session_id: Option, + #[serde(default)] + pub task_id: Option, + pub task_kind: String, + #[serde(default)] + pub idempotency_key: Option, + #[serde(default)] + pub title: Option, + #[serde(default)] + pub prompt: Option, + #[serde(default)] + pub input: Option, + #[serde(default)] + pub expected_output: Option, + #[serde(default)] + pub required_capabilities: Vec, + #[serde(default)] + pub capability_hints: Vec, + #[serde(default)] + pub knowledge_bindings: Vec, + #[serde(default)] + pub human_review: Option, + #[serde(default)] + pub event_name: Option, + #[serde(default)] + pub turn_id: Option, + #[serde(default)] + pub provider_preference: Option, + #[serde(default)] + pub model_preference: Option, + #[serde(default)] + pub queue_if_busy: Option, + #[serde(default)] + pub skip_pre_submit_resume: Option, + #[serde(default)] + pub run_start_hooks: Option, + #[serde(default)] + pub metadata: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentAppRuntimeStartTaskResult { + pub app_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub entry_key: Option, + pub task_id: String, + pub trace_id: String, + pub task_kind: String, + pub session_id: String, + pub turn_id: String, + pub event_name: String, + pub status: String, + pub submitted_at: String, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentAppRuntimeCancelTaskRequest { + pub app_id: String, + pub task_id: String, + pub session_id: String, + #[serde(default)] + pub turn_id: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentAppRuntimeCancelTaskResult { + pub app_id: String, + pub task_id: String, + pub session_id: String, + pub cancelled: bool, + pub status: String, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentAppRuntimeGetTaskRequest { + pub app_id: String, + pub task_id: String, + pub session_id: String, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentAppRuntimeTaskEvent { + pub id: String, + pub event_type: String, + pub status: String, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub severity: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub turn_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub request_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub evidence_ref: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub artifact_ref: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub occurred_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub payload: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentAppRuntimeTaskSnapshot { + pub app_id: String, + pub task_id: String, + pub session_id: String, + pub status: String, + pub task_status: String, + pub task_events: Vec, + pub thread_read: Value, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentAppRuntimeSubmitHostResponseRequest { + pub app_id: String, + pub task_id: String, + pub runtime_request: AgentRuntimeRespondActionRequest, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentAppRuntimeSubmitHostResponseResult { + pub app_id: String, + pub task_id: String, + pub status: String, +} diff --git a/src-tauri/src/commands/aster_agent_cmd/command_api/runtime_api.rs b/src-tauri/src/commands/aster_agent_cmd/command_api/runtime_api.rs index 00223abcf..565d07ef4 100644 --- a/src-tauri/src/commands/aster_agent_cmd/command_api/runtime_api.rs +++ b/src-tauri/src/commands/aster_agent_cmd/command_api/runtime_api.rs @@ -29,13 +29,14 @@ use lime_core::database::dao::agent_run::{AgentRun, AgentRunDao}; use lime_core::database::dao::agent_timeline::{AgentThreadItemStatus, AgentThreadTurnStatus}; use serde_json::{json, Value}; use std::path::PathBuf; -use std::time::Instant; +use std::time::{Duration, Instant}; use tauri::Manager; const RUNTIME_INTERRUPT_MESSAGE: &str = "用户已停止当前执行"; const RUNTIME_SESSION_OPEN_HISTORY_LIMIT: usize = 40; const RUNTIME_SESSION_MAX_HISTORY_LIMIT: usize = 2_000; +const TOOL_INVENTORY_AUX_TIMEOUT: Duration = Duration::from_secs(3); fn normalize_runtime_session_history_limit(history_limit: Option) -> Option { match history_limit { @@ -1422,17 +1423,6 @@ pub async fn agent_runtime_get_tool_inventory( mcp_manager: State<'_, McpManagerState>, request: Option, ) -> Result { - if state.is_initialized().await { - ensure_runtime_support_tools_registered( - &app, - state.inner(), - db.inner(), - api_key_provider_service.inner(), - mcp_manager.inner(), - ) - .await?; - } - let request = request.unwrap_or_default(); let caller = lime_core::tool_calling::normalize_tool_caller(request.caller.as_deref()) .unwrap_or_else(|| "assistant".to_string()); @@ -1442,35 +1432,84 @@ pub async fn agent_runtime_get_tool_inventory( (false, true) => WorkspaceToolSurface::browser_assist(), (false, false) => WorkspaceToolSurface::core(), }; - let mut warnings = Vec::new(); + if state.is_initialized().await { + match tokio::time::timeout( + TOOL_INVENTORY_AUX_TIMEOUT, + ensure_runtime_support_tools_registered( + &app, + state.inner(), + db.inner(), + api_key_provider_service.inner(), + mcp_manager.inner(), + ), + ) + .await + { + Ok(Ok(())) => {} + Ok(Err(error)) => warnings.push(format!("同步 runtime support tools 失败: {error}")), + Err(_) => warnings + .push("同步 runtime support tools 超时,已使用当前 registry 快照".to_string()), + } + } + let (mcp_server_names, mcp_tools) = { let manager = mcp_manager.lock().await; - let server_names = manager.get_running_servers().await; - let tools = match manager.list_tools().await { - Ok(tools) => tools, - Err(error) => { - warnings.push(format!("读取 MCP 工具列表失败: {error}")); - Vec::new() - } - }; + let server_names = + match tokio::time::timeout(TOOL_INVENTORY_AUX_TIMEOUT, manager.get_running_servers()) + .await + { + Ok(server_names) => server_names, + Err(_) => { + warnings.push("读取 MCP 服务列表超时,已跳过 MCP 服务快照".to_string()); + Vec::new() + } + }; + let tools = + match tokio::time::timeout(TOOL_INVENTORY_AUX_TIMEOUT, manager.list_tools()).await { + Ok(Ok(tools)) => tools, + Ok(Err(error)) => { + warnings.push(format!("读取 MCP 工具列表失败: {error}")); + Vec::new() + } + Err(_) => { + warnings.push("读取 MCP 工具列表超时,已跳过 MCP 工具快照".to_string()); + Vec::new() + } + }; (server_names, tools) }; let agent_arc = state.get_agent_arc(); - let guard = agent_arc.read().await; + let guard = match tokio::time::timeout(TOOL_INVENTORY_AUX_TIMEOUT, agent_arc.read()).await { + Ok(guard) => guard, + Err(_) => { + warnings.push("读取 Aster Agent 状态超时,runtime registry 快照为空".to_string()); + return Ok(build_tool_inventory(AgentToolInventoryBuildInput { + surface, + caller, + agent_initialized: false, + warnings, + persisted_execution_policy: Some(config_manager.config().agent.tool_execution), + request_metadata: request.metadata.clone(), + mcp_server_names, + mcp_tools, + registry_definitions: Vec::new(), + current_surface_tool_names: Vec::new(), + extension_configs: Vec::new(), + visible_extension_tools: Vec::new(), + searchable_extension_tools: Vec::new(), + })); + } + }; let Some(agent) = guard.as_ref() else { + warnings.push("Aster Agent 尚未初始化,runtime registry / extension 快照为空".to_string()); return Ok(build_tool_inventory(AgentToolInventoryBuildInput { surface, caller, agent_initialized: false, - warnings: { - warnings.push( - "Aster Agent 尚未初始化,runtime registry / extension 快照为空".to_string(), - ); - warnings - }, + warnings, persisted_execution_policy: Some(config_manager.config().agent.tool_execution), request_metadata: request.metadata.clone(), mcp_server_names, @@ -1484,7 +1523,28 @@ pub async fn agent_runtime_get_tool_inventory( }; let registry_arc = agent.tool_registry().clone(); - let registry = registry_arc.read().await; + let registry = match tokio::time::timeout(TOOL_INVENTORY_AUX_TIMEOUT, registry_arc.read()).await + { + Ok(registry) => registry, + Err(_) => { + warnings.push("读取 runtime registry 超时,已返回空 registry 快照".to_string()); + return Ok(build_tool_inventory(AgentToolInventoryBuildInput { + surface, + caller, + agent_initialized: true, + warnings, + persisted_execution_policy: Some(config_manager.config().agent.tool_execution), + request_metadata: request.metadata.clone(), + mcp_server_names, + mcp_tools, + registry_definitions: Vec::new(), + current_surface_tool_names: Vec::new(), + extension_configs: Vec::new(), + visible_extension_tools: Vec::new(), + searchable_extension_tools: Vec::new(), + })); + } + }; let mut registry_definitions = registry.get_definitions(); drop(registry); @@ -1506,33 +1566,50 @@ pub async fn agent_runtime_get_tool_inventory( let extension_configs = agent.get_extension_configs().await; let extension_manager = agent.extension_manager.clone(); - let visible_extension_tools = match extension_manager.get_prefixed_tools(None).await { - Ok(tools) => tools + let visible_extension_tools = match tokio::time::timeout( + TOOL_INVENTORY_AUX_TIMEOUT, + extension_manager.get_prefixed_tools(None), + ) + .await + { + Ok(Ok(tools)) => tools .into_iter() .map(|tool| ExtensionToolInventorySeed { name: tool.name.to_string(), description: tool.description.clone().unwrap_or_default().to_string(), }) .collect(), - Err(error) => { + Ok(Err(error)) => { warnings.push(format!("读取已加载 extension tools 失败: {error}")); Vec::new() } + Err(_) => { + warnings.push("读取已加载 extension tools 超时,已跳过 extension 工具快照".to_string()); + Vec::new() + } + }; + let searchable_extension_tools = match tokio::time::timeout( + TOOL_INVENTORY_AUX_TIMEOUT, + extension_manager.get_prefixed_tools_for_search(None), + ) + .await + { + Ok(Ok(tools)) => tools + .into_iter() + .map(|tool| ExtensionToolInventorySeed { + name: tool.name.to_string(), + description: tool.description.clone().unwrap_or_default().to_string(), + }) + .collect(), + Ok(Err(error)) => { + warnings.push(format!("读取 extension 搜索工具面失败: {error}")); + Vec::new() + } + Err(_) => { + warnings.push("读取 extension 搜索工具面超时,已跳过 extension 搜索快照".to_string()); + Vec::new() + } }; - let searchable_extension_tools = - match extension_manager.get_prefixed_tools_for_search(None).await { - Ok(tools) => tools - .into_iter() - .map(|tool| ExtensionToolInventorySeed { - name: tool.name.to_string(), - description: tool.description.clone().unwrap_or_default().to_string(), - }) - .collect(), - Err(error) => { - warnings.push(format!("读取 extension 搜索工具面失败: {error}")); - Vec::new() - } - }; Ok(build_tool_inventory(AgentToolInventoryBuildInput { surface, diff --git a/src-tauri/src/commands/aster_agent_cmd/runtime_turn.rs b/src-tauri/src/commands/aster_agent_cmd/runtime_turn.rs index b834a080b..7ab2f7f17 100644 --- a/src-tauri/src/commands/aster_agent_cmd/runtime_turn.rs +++ b/src-tauri/src/commands/aster_agent_cmd/runtime_turn.rs @@ -35,7 +35,8 @@ use lime_core::database::dao::agent_timeline::{AgentThreadItemPayload, AgentThre use lime_core::workspace::WorkspaceSettings; use regex::Regex; use serde_json::{json, Value}; -use std::collections::{HashMap, HashSet}; +use std::collections::{hash_map::DefaultHasher, HashMap, HashSet}; +use std::hash::{Hash, Hasher}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, OnceLock}; use std::time::{Duration, Instant}; @@ -324,6 +325,43 @@ fn build_runtime_projection_task_event( task_event } +fn runtime_projection_stream_event_id(prefix: &str, text: &str) -> String { + let mut hasher = DefaultHasher::new(); + prefix.hash(&mut hasher); + text.hash(&mut hasher); + chrono::Utc::now() + .timestamp_nanos_opt() + .unwrap_or_default() + .hash(&mut hasher); + format!("{prefix}:{:x}", hasher.finish()) +} + +fn build_runtime_projection_stream_task_event( + id_prefix: &'static str, + event_type: &'static str, + stream_kind: &'static str, + status: &'static str, + text: &str, + runtime_event: Value, +) -> Option { + if text.is_empty() { + return None; + } + let mut payload = serde_json::Map::new(); + payload.insert("streamKind".to_string(), json!(stream_kind)); + payload.insert("delta".to_string(), json!(text)); + payload.insert("runtimeEvent".to_string(), runtime_event); + let mut task_event = build_runtime_projection_task_event( + runtime_projection_stream_event_id(id_prefix, text), + event_type, + status, + text, + Value::Object(payload), + ); + task_event.insert("streamKind".to_string(), json!(stream_kind)); + Some(Value::Object(task_event)) +} + fn build_runtime_projection_artifact_task_event( id: String, status: impl Into, @@ -431,6 +469,136 @@ fn build_agent_app_runtime_event_primary_task_event( runtime_event: Value, ) -> Option { match event { + RuntimeAgentEvent::TextDelta { text } => build_runtime_projection_stream_task_event( + "runtime:text", + "task:partialArtifact", + "assistant_text_delta", + "streaming", + text, + runtime_event, + ), + RuntimeAgentEvent::TextDeltaBatch { text, .. } => { + build_runtime_projection_stream_task_event( + "runtime:text-batch", + "task:partialArtifact", + "assistant_text_batch", + "streaming", + text, + runtime_event, + ) + } + RuntimeAgentEvent::ThinkingDelta { text } => build_runtime_projection_stream_task_event( + "runtime:thinking", + "task:progress", + "thinking_delta", + "thinking", + text, + runtime_event, + ), + RuntimeAgentEvent::ToolInputDelta { + tool_id, + tool_name, + delta, + .. + } => { + let mut event = build_runtime_projection_stream_task_event( + "runtime:tool-input", + "task:toolCall", + "tool_input_delta", + "streaming", + delta, + runtime_event, + )?; + if let Some(task_event) = event.as_object_mut() { + task_event.insert("toolId".to_string(), json!(tool_id)); + if let Some(tool_name) = tool_name { + task_event.insert("toolName".to_string(), json!(tool_name)); + } + } + Some(event) + } + RuntimeAgentEvent::ToolOutputDelta { + tool_id, + delta, + output_kind, + .. + } => { + let mut event = build_runtime_projection_stream_task_event( + "runtime:tool-output", + "task:toolCall", + "tool_output_delta", + "streaming", + delta, + runtime_event, + )?; + if let Some(task_event) = event.as_object_mut() { + task_event.insert("toolId".to_string(), json!(tool_id)); + if let Some(output_kind) = output_kind { + task_event.insert("toolName".to_string(), json!(output_kind)); + } + } + Some(event) + } + RuntimeAgentEvent::TaskProfileResolved { task_profile } => { + Some(Value::Object(build_runtime_projection_task_event( + format!("runtime:task-profile:{}", task_profile.kind), + "task:progress", + "routing", + format!("已识别任务类型:{}", task_profile.kind), + runtime_event, + ))) + } + RuntimeAgentEvent::CandidateSetResolved { routing_decision } => { + Some(Value::Object(build_runtime_projection_task_event( + "runtime:routing:candidates".to_string(), + "task:progress", + "routing", + format!("已找到 {} 个候选模型", routing_decision.candidate_count), + runtime_event, + ))) + } + RuntimeAgentEvent::RoutingDecisionMade { routing_decision } => { + let selected = routing_decision + .selected_provider + .as_deref() + .zip(routing_decision.selected_model.as_deref()) + .map(|(provider, model)| format!("{provider}/{model}")) + .or_else(|| routing_decision.selected_model.clone()) + .or_else(|| routing_decision.selected_provider.clone()) + .unwrap_or_else(|| "自动选择".to_string()); + Some(Value::Object(build_runtime_projection_task_event( + "runtime:routing:decision".to_string(), + "task:progress", + "routing", + format!("模型路由已确定:{selected}"), + runtime_event, + ))) + } + RuntimeAgentEvent::RoutingFallbackApplied { routing_decision } => { + let selected = routing_decision + .selected_model + .clone() + .or_else(|| routing_decision.selected_provider.clone()) + .unwrap_or_else(|| "备用模型".to_string()); + Some(Value::Object(build_runtime_projection_task_event( + "runtime:routing:fallback".to_string(), + "task:incident", + "warning", + format!("模型路由已回退到:{selected}"), + runtime_event, + ))) + } + RuntimeAgentEvent::RoutingNotPossible { routing_decision } => { + let mut task_event = build_runtime_projection_task_event( + "runtime:routing:not-possible".to_string(), + "task:error", + "failed", + routing_decision.decision_reason.clone(), + runtime_event, + ); + task_event.insert("severity".to_string(), json!("error")); + Some(Value::Object(task_event)) + } RuntimeAgentEvent::RuntimeStatus { status } => { Some(Value::Object(build_runtime_projection_task_event( format!("runtime:status:{}", status.phase), @@ -2074,6 +2242,7 @@ struct RuntimeTurnSubmitBootstrap { provider_continuation_capability: ProviderContinuationCapability, tracker: ExecutionTracker, model_skill_tool_enabled: bool, + model_skill_tool_allowed_skill_names: Option>, model_skill_tool_allowed_skill_sources: Option>, } @@ -2474,6 +2643,82 @@ impl RuntimeTurnSubmitPreparation { .model_skill_tool_allowed_skill_sources .clone() } + + fn model_skill_tool_allowed_skill_names(&self) -> Option> { + self.submit_bootstrap + .model_skill_tool_allowed_skill_names + .clone() + } +} + +fn collect_required_skill_names_from_contract_object( + contract: &serde_json::Map, +) -> Vec { + let mut names = Vec::new(); + let mut seen = HashSet::new(); + + for key in [ + "required_skills", + "requiredSkills", + "skill_refs", + "skillRefs", + "skills", + ] { + let Some(items) = contract.get(key).and_then(serde_json::Value::as_array) else { + continue; + }; + for item in items { + let required = item + .as_object() + .and_then(|object| object.get("required")) + .and_then(serde_json::Value::as_bool) + .unwrap_or(true); + if !required { + continue; + } + let name = item + .as_str() + .map(str::to_string) + .or_else(|| { + item.as_object().and_then(|object| { + ["skill", "id", "name"] + .iter() + .filter_map(|key| object.get(*key)) + .find_map(serde_json::Value::as_str) + .map(str::to_string) + }) + }) + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()); + let Some(name) = name else { + continue; + }; + if seen.insert(name.to_ascii_lowercase()) { + names.push(name); + } + } + } + + names +} + +fn resolve_agent_app_required_skill_tool_allowlist( + request_metadata: Option<&serde_json::Value>, +) -> Option> { + for key in [ + "content_factory_skill_contract", + "agent_app_runtime_skill_contract", + ] { + let Some(contract) = extract_harness_nested_object(request_metadata, &[key]) else { + continue; + }; + let names = collect_required_skill_names_from_contract_object(contract); + if !names.is_empty() { + return Some(names); + } + } + + None } #[allow(clippy::too_many_arguments)] @@ -2627,6 +2872,8 @@ async fn prepare_runtime_turn_submit_bootstrap( request_metadata.as_ref(), workspace_root, )?; + let agent_app_required_skill_names = + resolve_agent_app_required_skill_tool_allowlist(request_metadata.as_ref()); let model_skill_tool_allowed_skill_sources = workspace_skill_runtime_enable.as_ref().map(|projection| { projection @@ -2659,7 +2906,9 @@ async fn prepare_runtime_turn_submit_bootstrap( Ok(RuntimeTurnSubmitBootstrap { model_skill_tool_enabled: matches!(execution_profile, TurnExecutionProfile::FullRuntime) && (should_enable_model_skill_tool(request_metadata.as_ref()) - || workspace_skill_runtime_enable.is_some()), + || workspace_skill_runtime_enable.is_some() + || agent_app_required_skill_names.is_some()), + model_skill_tool_allowed_skill_names: agent_app_required_skill_names, model_skill_tool_allowed_skill_sources, request_metadata, runtime_memory_config: runtime_config.memory.clone(), @@ -3740,12 +3989,15 @@ async fn execute_runtime_turn_with_session_scope( submit_preparation: RuntimeTurnSubmitPreparation, ) -> Result<(), String> { let model_skill_tool_enabled = submit_preparation.model_skill_tool_enabled(); + let model_skill_tool_allowed_skill_names = + submit_preparation.model_skill_tool_allowed_skill_names(); let model_skill_tool_allowed_skill_sources = submit_preparation.model_skill_tool_allowed_skill_sources(); with_runtime_turn_session_scope( state, session_id, model_skill_tool_enabled, + model_skill_tool_allowed_skill_names, model_skill_tool_allowed_skill_sources, move |cancel_token| async move { execute_runtime_turn_submit( @@ -7390,6 +7642,7 @@ async fn with_runtime_turn_session_scope( state: &AsterAgentState, session_id: &str, skill_tool_access_enabled: bool, + skill_tool_allowed_skill_names: Option>, skill_tool_allowed_skill_sources: Option>, run: F, ) -> Result<(), String> @@ -7403,6 +7656,8 @@ where session_id, allowed_skill_sources, ); + } else if let Some(allowed_skill_names) = skill_tool_allowed_skill_names { + lime_agent::tools::set_skill_tool_session_allowed_skills(session_id, allowed_skill_names); } else { lime_agent::tools::set_skill_tool_session_access(session_id, skill_tool_access_enabled); } @@ -9056,16 +9311,39 @@ mod tests { } #[test] - fn agent_app_runtime_runtime_event_projection_skips_low_value_text_delta() { + fn agent_app_runtime_runtime_event_projection_builds_stream_text_task_event() { let event = RuntimeAgentEvent::TextDelta { - text: "partial".to_string(), + text: "第一段真实输出".to_string(), }; - assert!(build_agent_app_runtime_event_projection_payload( + let payload = build_agent_app_runtime_event_projection_payload( "agent_app_runtime:content-factory-app:task-1", &event, ) - .is_none()); + .expect("runtime stream projection payload"); + let task_event = payload + .get("taskEvents") + .and_then(Value::as_array) + .and_then(|events| events.first()) + .and_then(Value::as_object) + .expect("stream task event"); + + assert_eq!( + task_event.get("eventType"), + Some(&json!("task:partialArtifact")) + ); + assert_eq!(task_event.get("status"), Some(&json!("streaming"))); + assert_eq!(task_event.get("message"), Some(&json!("第一段真实输出"))); + assert_eq!( + task_event.get("streamKind"), + Some(&json!("assistant_text_delta")) + ); + assert_eq!( + task_event + .get("payload") + .and_then(|payload| payload.get("delta")), + Some(&json!("第一段真实输出")) + ); } #[test] @@ -10058,6 +10336,34 @@ mod tests { assert!(!should_enable_model_skill_tool(Some(&metadata))); } + #[test] + fn agent_app_skill_contract_should_resolve_required_skill_allowlist() { + let metadata = json!({ + "harness": { + "theme": "general", + "session_mode": "default", + "allow_model_skills": true, + "content_factory_skill_contract": { + "policy": "must_use_required_skills_before_final_patch", + "required_skills": [ + { "id": "article-writer", "skill": "article-writer", "required": true }, + { "id": "content-reviewer", "skill": "content-reviewer", "required": true }, + { "id": "draft-only", "skill": "draft-only", "required": false } + ] + } + } + }); + + assert_eq!( + resolve_agent_app_required_skill_tool_allowlist(Some(&metadata)), + Some(vec![ + "article-writer".to_string(), + "content-reviewer".to_string() + ]) + ); + assert!(should_enable_model_skill_tool(Some(&metadata))); + } + #[tokio::test] async fn enforce_runtime_turn_user_prompt_submit_hooks_should_allow_without_project_hooks() { let temp_dir = tempfile::TempDir::new().expect("create temp dir"); diff --git a/src-tauri/src/skills/default_skills.rs b/src-tauri/src/skills/default_skills.rs index f35ef8929..47f9b7824 100644 --- a/src-tauri/src/skills/default_skills.rs +++ b/src-tauri/src/skills/default_skills.rs @@ -10,6 +10,9 @@ use lime_core::models::{ ANALYSIS_SKILL_DIRECTORY, BRAND_PERSONA_KNOWLEDGE_BUILDER_SKILL_DIRECTORY, BRAND_PRODUCT_KNOWLEDGE_BUILDER_SKILL_DIRECTORY, BROADCAST_GENERATE_SKILL_DIRECTORY, CAMPAIGN_OPERATIONS_KNOWLEDGE_BUILDER_SKILL_DIRECTORY, + CONTENT_FACTORY_ARTICLE_WRITER_SKILL_DIRECTORY, + CONTENT_FACTORY_CONTENT_REVIEWER_SKILL_DIRECTORY, + CONTENT_FACTORY_KNOWLEDGE_BUILDER_SKILL_DIRECTORY, CONTENT_OPERATIONS_KNOWLEDGE_BUILDER_SKILL_DIRECTORY, CONTENT_POST_WITH_COVER_SKILL_DIRECTORY, COVER_GENERATE_SKILL_DIRECTORY, FORM_GENERATE_SKILL_DIRECTORY, GROWTH_STRATEGY_KNOWLEDGE_BUILDER_SKILL_DIRECTORY, IMAGE_GENERATE_SKILL_DIRECTORY, @@ -93,6 +96,15 @@ const CONTENT_POST_WITH_COVER_SKILL_CONTENT: &str = const CONTENT_POST_WITH_COVER_WORKFLOW_CONTENT: &str = include_str!("../../resources/default-skills/content_post_with_cover/references/workflow.json"); +const CONTENT_FACTORY_KNOWLEDGE_BUILDER_SKILL_CONTENT: &str = + include_str!("../../resources/default-skills/knowledge-builder/SKILL.md"); + +const CONTENT_FACTORY_ARTICLE_WRITER_SKILL_CONTENT: &str = + include_str!("../../resources/default-skills/article-writer/SKILL.md"); + +const CONTENT_FACTORY_CONTENT_REVIEWER_SKILL_CONTENT: &str = + include_str!("../../resources/default-skills/content-reviewer/SKILL.md"); + const KNOWLEDGE_BUILDER_SKILL_CONTENT: &str = include_str!("../../resources/default-skills/knowledge_builder/SKILL.md"); @@ -391,7 +403,7 @@ const GROWTH_STRATEGY_KNOWLEDGE_BUILDER_EXTRA_FILES: &[BundledSkillFile] = &[ }, ]; -fn default_skills() -> [BundledSkillDefinition; 30] { +fn default_skills() -> [BundledSkillDefinition; 33] { [ BundledSkillDefinition { directory: VIDEO_GENERATE_SKILL_DIRECTORY, @@ -493,6 +505,21 @@ fn default_skills() -> [BundledSkillDefinition; 30] { skill_content: CONTENT_POST_WITH_COVER_SKILL_CONTENT, extra_files: CONTENT_POST_WITH_COVER_EXTRA_FILES, }, + BundledSkillDefinition { + directory: CONTENT_FACTORY_KNOWLEDGE_BUILDER_SKILL_DIRECTORY, + skill_content: CONTENT_FACTORY_KNOWLEDGE_BUILDER_SKILL_CONTENT, + extra_files: &[], + }, + BundledSkillDefinition { + directory: CONTENT_FACTORY_ARTICLE_WRITER_SKILL_DIRECTORY, + skill_content: CONTENT_FACTORY_ARTICLE_WRITER_SKILL_CONTENT, + extra_files: &[], + }, + BundledSkillDefinition { + directory: CONTENT_FACTORY_CONTENT_REVIEWER_SKILL_DIRECTORY, + skill_content: CONTENT_FACTORY_CONTENT_REVIEWER_SKILL_CONTENT, + extra_files: &[], + }, BundledSkillDefinition { directory: KNOWLEDGE_BUILDER_SKILL_DIRECTORY, skill_content: KNOWLEDGE_BUILDER_SKILL_CONTENT, diff --git a/src/components/AppSidebar.test.tsx b/src/components/AppSidebar.test.tsx index db0073ae9..cc9893f95 100644 --- a/src/components/AppSidebar.test.tsx +++ b/src/components/AppSidebar.test.tsx @@ -573,6 +573,55 @@ describe("AppSidebar", () => { ).not.toBeNull(); }); + it("进入 Agent App 运行页时应临时折叠导航栏并在离开后恢复", async () => { + localStorage.setItem(APP_SIDEBAR_COLLAPSED_STORAGE_KEY, "false"); + const onNavigate = vi.fn(); + const mounted = mountSidebar({ + currentPage: "agent-app", + onNavigate, + }); + await flushEffects(2); + + expect( + mounted.container + .querySelector('[data-testid="app-sidebar"]') + ?.getAttribute("data-collapsed"), + ).toBe("true"); + expect(localStorage.getItem(APP_SIDEBAR_COLLAPSED_STORAGE_KEY)).toBe( + "false", + ); + + await act(async () => { + mounted.container + .querySelector('button[aria-label="展开导航栏"]') + ?.click(); + await Promise.resolve(); + }); + await flushEffects(2); + + expect( + mounted.container + .querySelector('[data-testid="app-sidebar"]') + ?.getAttribute("data-collapsed"), + ).toBe("false"); + expect(localStorage.getItem(APP_SIDEBAR_COLLAPSED_STORAGE_KEY)).toBe( + "false", + ); + + act(() => { + mounted.root.render( + , + ); + }); + await flushEffects(2); + + expect( + mounted.container + .querySelector('[data-testid="app-sidebar"]') + ?.getAttribute("data-collapsed"), + ).toBe("false"); + }); + it("默认应渲染一级主导航,并将系统入口收进用户弹框", async () => { const container = mountSidebarContainer({ currentPageParams: { diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index a67f19e98..a104da4fb 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -179,6 +179,7 @@ type SidebarNavItem = SidebarNavItemDefinition; const APP_SIDEBAR_COLLAPSED_STORAGE_KEY = "lime.app-sidebar.collapsed"; const APP_SIDEBAR_COLLAPSE_EVENT = "lime:app-sidebar-collapse"; +const AGENT_APP_RUNTIME_SIDEBAR_COLLAPSE_SOURCE = "agent-app-runtime"; const SIDEBAR_RECENT_SESSION_PAGE_SIZE = 10; const SIDEBAR_ARCHIVED_SESSION_PAGE_SIZE = 8; const SIDEBAR_SEARCH_RESULT_LIMIT = 8; @@ -2395,6 +2396,7 @@ export function AppSidebar({ ?.agentEntry; const activeAgentPageParams = activePageParams as AgentPageParams | undefined; const isAgentWorkspace = activePage === "agent"; + const isAgentAppRuntime = activePage === "agent-app"; const isClawTaskCenter = isAgentWorkspace && agentEntry === "claw"; const isNewTaskHome = activePage === "agent" && agentEntry === "new-task"; const [rememberedProjectId, setRememberedProjectId] = useState( @@ -2418,6 +2420,7 @@ export function AppSidebar({ }); const collapsedRef = useRef(collapsed); const collapseRestoreBySourceRef = useRef>({}); + const agentAppRuntimeSidebarManualOverrideRef = useRef(false); useEffect(() => { collapsedRef.current = collapsed; }, [collapsed]); @@ -3085,6 +3088,34 @@ export function AppSidebar({ setCollapsed(false); }, [isClawTaskCenter, isNewTaskHome]); + useEffect(() => { + const source = AGENT_APP_RUNTIME_SIDEBAR_COLLAPSE_SOURCE; + if (isAgentAppRuntime) { + if (!(source in collapseRestoreBySourceRef.current)) { + collapseRestoreBySourceRef.current[source] = collapsedRef.current; + agentAppRuntimeSidebarManualOverrideRef.current = false; + } + if (!agentAppRuntimeSidebarManualOverrideRef.current) { + setCollapsed(true); + } + return; + } + + agentAppRuntimeSidebarManualOverrideRef.current = false; + const previous = collapseRestoreBySourceRef.current[source]; + delete collapseRestoreBySourceRef.current[source]; + if (typeof previous === "boolean") { + setCollapsed(previous); + } + }, [isAgentAppRuntime]); + + const toggleSidebarCollapsed = useCallback(() => { + if (isAgentAppRuntime) { + agentAppRuntimeSidebarManualOverrideRef.current = true; + } + setCollapsed((value) => !value); + }, [isAgentAppRuntime]); + const shouldShowConversationList = !collapsed && !(activePage === "agent" && activeAgentPageParams?.immersiveHome); @@ -4735,6 +4766,7 @@ export function AppSidebar({ $themeMode={themeState.effectiveThemeMode} $reserveWindowControls={reserveWindowControls} data-testid="app-sidebar" + data-collapsed={String(collapsed)} data-lime-window-drag-region data-window-controls-reserved={String(reserveWindowControls)} onMouseDown={onStartWindowDrag} @@ -4785,7 +4817,7 @@ export function AppSidebar({ {maybeWrapWithTooltip( setCollapsed((value) => !value)} + onClick={toggleSidebarCollapsed} title={navigationToggleLabel} aria-label={navigationToggleLabel} > diff --git a/src/features/agent-app/adapters/adapterCapabilityProfile.ts b/src/features/agent-app/adapters/adapterCapabilityProfile.ts index 7a5461bed..5ba6275df 100644 --- a/src/features/agent-app/adapters/adapterCapabilityProfile.ts +++ b/src/features/agent-app/adapters/adapterCapabilityProfile.ts @@ -1,27 +1,8 @@ import { resolveAgentAppHostFlags } from "../featureFlag"; import { p0HostCapabilityProfile } from "../readiness/hostCapabilityProfile"; +import { buildLimeCapabilityProfileEntriesForMode } from "../sdk/capabilityCatalog"; import type { AgentAppHostFlags, HostCapabilityProfile } from "../types"; -const HYBRID_CAPABILITIES = [ - "lime.ui", - "lime.storage", - "lime.files", - "lime.artifacts", - "lime.evidence", - "lime.agent", - "lime.knowledge", - "lime.tools", - "lime.workflow", -] as const; - -const ADAPTER_CAPABILITIES = new Set([ - "lime.storage", - "lime.artifacts", - "lime.evidence", - "lime.agent", - "lime.knowledge", -]); - export function buildAdapterCapabilityProfile( flagOverrides: Partial = {}, ): HostCapabilityProfile { @@ -32,21 +13,10 @@ export function buildAdapterCapabilityProfile( localStorageEnabled: true, mockSdkEnabled: false, }); - const capabilities: HostCapabilityProfile["capabilities"] = { - ...p0HostCapabilityProfile.capabilities, - }; - - HYBRID_CAPABILITIES.forEach((capability) => { - capabilities[capability] = { - version: capabilities[capability]?.version ?? "0.3.0", - enabled: true, - implementation: ADAPTER_CAPABILITIES.has(capability) ? "adapter" : "mock", - }; - }); return { ...p0HostCapabilityProfile, - capabilities, + capabilities: buildLimeCapabilityProfileEntriesForMode("adapter"), featureFlags, }; } diff --git a/src/features/agent-app/index.ts b/src/features/agent-app/index.ts index 035cd7460..a8d0da655 100644 --- a/src/features/agent-app/index.ts +++ b/src/features/agent-app/index.ts @@ -113,7 +113,27 @@ export type { LimeCapabilityErrorContext, } from "./sdk/capabilityErrors"; export { + LIME_CAPABILITY_DEFINITIONS, + LIME_CAPABILITY_GROUPS, LIME_CAPABILITY_NAMES, + buildLimeCapabilityProfileEntries, + buildLimeCapabilityProfileEntriesForMode, + getLimeCapabilityAdapterKey, + getLimeCapabilityDefinition, + listEnabledLimeCapabilityNamesForMode, +} from "./sdk/capabilityCatalog"; +export type { + LimeCapabilityDefinition, + LimeCapabilityAdapterKey, + LimeCapabilityDefinitionRecord, + LimeCapabilityGroup, + LimeCapabilityMethodName, + LimeCapabilityOwner, + LimeCapabilityProfileEntry, + LimeCapabilityStage, +} from "./sdk/capabilityCatalog"; + +export { buildLimeCapabilityInvokeProvenance, buildLimeCapabilityInvokeRequest, createLimeCapabilityErrorResponse, @@ -146,13 +166,37 @@ export type { CreateLimeCoreCapabilityAdaptersOptions, LimeAgentCapabilityAdapter, LimeArtifactsCapabilityAdapter, + LimeAutomationCapabilityAdapter, + LimeBrowserCapabilityAdapter, + LimeCapabilitiesCapabilityAdapter, LimeCapabilityAdapterCallOptions, + LimeCapabilityAdapter, + LimeConnectorsCapabilityAdapter, + LimeContextCapabilityAdapter, LimeCoreCapabilityAdapters, + LimeDocumentsCapabilityAdapter, LimeEvidenceCapabilityAdapter, + LimeEventsCapabilityAdapter, + LimeFilesCapabilityAdapter, LimeKnowledgeCapabilityAdapter, + LimeMcpCapabilityAdapter, + LimeMediaCapabilityAdapter, + LimeMemoryCapabilityAdapter, + LimeModelsCapabilityAdapter, + LimePolicyCapabilityAdapter, + LimeReviewCapabilityAdapter, + LimeSearchCapabilityAdapter, + LimeSecretsCapabilityAdapter, + LimeSettingsCapabilityAdapter, + LimeSkillsCapabilityAdapter, LimeStorageCapabilityAdapter, + LimeTasksCapabilityAdapter, + LimeTerminalCapabilityAdapter, LimeToolsCapabilityAdapter, LimeUiCapabilityAdapter, + LimeUsageCapabilityAdapter, + LimeWorkflowCapabilityAdapter, + LimeWorkspaceCapabilityAdapter, } from "./sdk/capabilityAdapters"; export { LIME_AGENT_APP_BRIDGE_PROTOCOL, @@ -199,9 +243,7 @@ export type { AgentAppRuntimePackagePolicyEvidence, AgentAppUiBundleDescriptor, } from "./runtime/runtimePackageLoader"; -export { - evaluateAgentAppEntryRuntimeGuard, -} from "./runtime/entryRuntimeGuard"; +export { evaluateAgentAppEntryRuntimeGuard } from "./runtime/entryRuntimeGuard"; export type { AgentAppEntryRuntimeGuardIssue, AgentAppEntryRuntimeLifecycleState, diff --git a/src/features/agent-app/manifest/normalizeManifest.ts b/src/features/agent-app/manifest/normalizeManifest.ts index f209ba5e7..3e0881f2e 100644 --- a/src/features/agent-app/manifest/normalizeManifest.ts +++ b/src/features/agent-app/manifest/normalizeManifest.ts @@ -16,7 +16,18 @@ function slugifyAppId(value: string): string { return normalized || "agent-app"; } -function normalizeManifestVersion(version: string): "0.2" | "0.3" { +function normalizeManifestVersion( + version: string, +): "0.2" | "0.3" | "0.5" | "0.6" | "0.7" { + if (version.startsWith("0.7")) { + return "0.7"; + } + if (version.startsWith("0.6")) { + return "0.6"; + } + if (version.startsWith("0.5")) { + return "0.5"; + } if (version.startsWith("0.3")) { return "0.3"; } @@ -34,17 +45,24 @@ function normalizeCapabilities( } if (Array.isArray(capabilities)) { - return Object.fromEntries(capabilities.map((capability) => [capability, "*"])); + return Object.fromEntries( + capabilities.map((capability) => [capability, "*"]), + ); } return Object.fromEntries( - Object.entries(capabilities).filter(([capability]) => capability.trim().length > 0), + Object.entries(capabilities).filter( + ([capability]) => capability.trim().length > 0, + ), ); } function normalizeEntry(entry: AppEntry): NormalizedAppEntry { const requiredCapabilities = Array.from( - new Set([...(entry.requiredCapabilities ?? []), ...(entry.capabilities ?? [])]), + new Set([ + ...(entry.requiredCapabilities ?? []), + ...(entry.capabilities ?? []), + ]), ); return { @@ -61,12 +79,16 @@ function normalizeEntry(entry: AppEntry): NormalizedAppEntry { }; } -export function normalizeManifest(manifest: AppManifest): NormalizedAppManifest { +export function normalizeManifest( + manifest: AppManifest, +): NormalizedAppManifest { const appId = slugifyAppId(manifest.name); - const requiresCapabilities = normalizeCapabilities(manifest.requires?.capabilities); + const requiresCapabilities = normalizeCapabilities( + manifest.requires?.capabilities, + ); const topLevelCapabilities = Object.fromEntries( - Object.entries(normalizeCapabilities(manifest.capabilities)).filter(([capability]) => - capability.startsWith("lime."), + Object.entries(normalizeCapabilities(manifest.capabilities)).filter( + ([capability]) => capability.startsWith("lime."), ), ); const storageRetention = @@ -115,5 +137,10 @@ export function normalizeManifest(manifest: AppManifest): NormalizedAppManifest overlayTemplates: manifest.overlayTemplates ?? [], ui: manifest.ui, lifecycle: manifest.lifecycle ?? {}, + agentRuntime: manifest.agentRuntime, + requirements: manifest.requirements, + boundary: manifest.boundary, + integrations: manifest.integrations, + operations: manifest.operations, }; } diff --git a/src/features/agent-app/manifest/parseManifest.test.ts b/src/features/agent-app/manifest/parseManifest.test.ts index 65ff7bbd4..f1cadb588 100644 --- a/src/features/agent-app/manifest/parseManifest.test.ts +++ b/src/features/agent-app/manifest/parseManifest.test.ts @@ -1,7 +1,11 @@ import { describe, expect, it } from "vitest"; import contentFactoryFixture from "../fixtures/content-factory-app.json"; import { normalizeManifest } from "./normalizeManifest"; -import { AgentAppManifestError, parseManifest } from "./parseManifest"; +import { + AgentAppManifestError, + mergeLayeredManifest, + parseManifest, +} from "./parseManifest"; describe("Agent App manifest P0", () => { it("应解析并归一化 内容工厂 v0.3 fixture", () => { @@ -44,8 +48,9 @@ describe("Agent App manifest P0", () => { "creative_capability_search", ]); expect( - normalized.toolRefs.find((tool) => tool.key === "creative_capability_search") - ?.capabilities, + normalized.toolRefs.find( + (tool) => tool.key === "creative_capability_search", + )?.capabilities, ).toEqual([ "lime.capability.image.generate", "lime.capability.cover.generate", @@ -99,4 +104,322 @@ describe("Agent App manifest P0", () => { enabledByDefault: true, }); }); + + it("应把 v0.6 capability list 归一化为稳定 capability range", () => { + const normalized = normalizeManifest( + parseManifest({ + manifestVersion: "0.6.0", + name: "Content Factory App", + version: "0.6.0", + requires: { + sdk: "@lime/app-sdk@^0.6.0", + capabilities: ["lime.agent", "lime.skills", "lime.usage"], + }, + entries: [{ key: "home", kind: "page" }], + }), + ); + + expect(normalized.manifestVersion).toBe("0.6"); + expect(normalized.requires.sdk).toBe("@lime/app-sdk@^0.6.0"); + expect(normalized.requires.capabilities).toMatchObject({ + "lime.agent": "*", + "lime.skills": "*", + "lime.usage": "*", + }); + }); + + it("应按 v0.5 分层 manifest 文件合并发现面和治理配置", () => { + const manifest = mergeLayeredManifest( + { + manifestVersion: "0.5.0", + name: "content-factory-app", + version: "0.5.0", + entries: [{ key: "dashboard", kind: "page", title: "首页" }], + permissions: [{ key: "read_selected_files", reason: "读取文件" }], + }, + [ + { + entries: [ + { + key: "dashboard", + kind: "page", + title: "内容工厂首页", + route: "/dashboard", + }, + { key: "settings", kind: "settings", title: "设置" }, + ], + }, + { + permissions: [ + { + key: "read_selected_files", + scope: "filesystem", + required: true, + }, + { key: "invoke_agent_tasks", scope: "agent", required: true }, + ], + }, + { + capabilities: { + "lime.ui": { + routes: [{ path: "/dashboard" }], + }, + }, + errors: { + model_setup_required: { + severity: "setup", + }, + }, + i18n: { + defaultLocale: "zh-CN", + }, + signature: { + mode: "development", + }, + readiness: { + checks: [{ key: "manifest_layers", required: true }], + }, + health: { + probes: [{ key: "ui_entry", path: "/dashboard" }], + }, + }, + ], + ); + const normalized = normalizeManifest(manifest); + + expect(normalized.manifestVersion).toBe("0.5"); + expect(manifest.entries).toEqual([ + { + key: "dashboard", + kind: "page", + title: "内容工厂首页", + route: "/dashboard", + }, + { key: "settings", kind: "settings", title: "设置" }, + ]); + expect(manifest.permissions).toEqual([ + { + key: "read_selected_files", + reason: "读取文件", + scope: "filesystem", + required: true, + }, + { key: "invoke_agent_tasks", scope: "agent", required: true }, + ]); + expect( + (manifest as unknown as Record).capabilityConfig, + ).toEqual({ + "lime.ui": { + routes: [{ path: "/dashboard" }], + }, + }); + expect((manifest as unknown as Record).errors).toEqual({ + model_setup_required: { + severity: "setup", + }, + }); + expect((manifest as unknown as Record).i18n).toEqual({ + defaultLocale: "zh-CN", + }); + expect((manifest as unknown as Record).signature).toEqual({ + mode: "development", + }); + expect((manifest as unknown as Record).readiness).toEqual({ + checks: [{ key: "manifest_layers", required: true }], + }); + expect((manifest as unknown as Record).health).toEqual({ + probes: [{ key: "ui_entry", path: "/dashboard" }], + }); + expect( + (manifest as unknown as Record).agentRuntime, + ).toBeUndefined(); + }); + + it("应按 v0.6 分层 manifest 文件合并入口、权限和运行合同", () => { + const manifest = mergeLayeredManifest( + { + manifestVersion: "0.6.0", + name: "content-factory-app", + version: "0.6.0", + entries: [{ key: "dashboard", kind: "page", title: "首页" }], + permissions: [{ key: "read_selected_files", reason: "读取文件" }], + }, + [ + { + entries: [ + { + key: "dashboard", + kind: "page", + title: "内容工厂首页", + route: "/dashboard", + }, + { key: "settings", kind: "settings", title: "设置" }, + ], + }, + { + permissions: [ + { + key: "read_selected_files", + scope: "filesystem", + required: true, + }, + { key: "invoke_agent_tasks", scope: "agent", required: true }, + ], + }, + { + capabilities: { + "lime.ui": { + routes: [{ path: "/dashboard" }], + }, + }, + i18n: { + defaultLocale: "zh-CN", + }, + agentRuntime: { + agentTask: { + eventSchema: "lime.agent-task-event.v1", + }, + }, + }, + ], + ); + + expect(manifest.entries).toEqual([ + { + key: "dashboard", + kind: "page", + title: "内容工厂首页", + route: "/dashboard", + }, + { key: "settings", kind: "settings", title: "设置" }, + ]); + expect(manifest.permissions).toEqual([ + { + key: "read_selected_files", + reason: "读取文件", + scope: "filesystem", + required: true, + }, + { key: "invoke_agent_tasks", scope: "agent", required: true }, + ]); + expect( + (manifest as unknown as Record).capabilityConfig, + ).toEqual({ + "lime.ui": { + routes: [{ path: "/dashboard" }], + }, + }); + expect((manifest as unknown as Record).i18n).toEqual({ + defaultLocale: "zh-CN", + }); + expect( + (manifest as unknown as Record).agentRuntime, + ).toEqual({ + agentTask: { + eventSchema: "lime.agent-task-event.v1", + }, + }); + }); + + it("应按 v0.7 分层 manifest 文件合并需求边界和能力交接", () => { + const manifest = mergeLayeredManifest( + { + manifestVersion: "0.7.0", + name: "content-factory-app", + version: "0.7.0", + entries: [{ key: "dashboard", kind: "page", title: "首页" }], + }, + [ + { + requirements: { + requirements: [ + { + id: "CF-R001", + text: "生成可审核内容草稿", + priority: "mvp", + }, + ], + nonGoals: ["直接保存外部系统明文凭证"], + }, + }, + { + boundaries: [ + { + requirementId: "CF-R001", + planes: { + app: { owns: ["workflow_state"] }, + host: { requires: ["lime.agent", "lime.evidence"] }, + human: { owns: ["publish_decision"] }, + }, + }, + ], + }, + { + integrations: [ + { + key: "planning_table", + provider: "cloud.table", + executionPlane: "hybrid", + hostCapability: "lime.connectors", + }, + ], + }, + { + operations: [ + { + key: "write_external_draft", + type: "external_write", + sideEffect: "external_write", + approvalRequired: true, + dryRunRequired: true, + evidenceRequired: true, + autoExecute: false, + }, + ], + }, + ], + ); + const normalized = normalizeManifest(manifest); + + expect(normalized.manifestVersion).toBe("0.7"); + expect(normalized.requirements).toEqual({ + requirements: [ + { + id: "CF-R001", + text: "生成可审核内容草稿", + priority: "mvp", + }, + ], + nonGoals: ["直接保存外部系统明文凭证"], + }); + expect(normalized.boundary).toEqual([ + { + requirementId: "CF-R001", + planes: { + app: { owns: ["workflow_state"] }, + host: { requires: ["lime.agent", "lime.evidence"] }, + human: { owns: ["publish_decision"] }, + }, + }, + ]); + expect(normalized.integrations).toEqual([ + { + key: "planning_table", + provider: "cloud.table", + executionPlane: "hybrid", + hostCapability: "lime.connectors", + }, + ]); + expect(normalized.operations).toEqual([ + { + key: "write_external_draft", + type: "external_write", + sideEffect: "external_write", + approvalRequired: true, + dryRunRequired: true, + evidenceRequired: true, + autoExecute: false, + }, + ]); + }); }); diff --git a/src/features/agent-app/manifest/parseManifest.ts b/src/features/agent-app/manifest/parseManifest.ts index 91c86a3b1..80acd01b4 100644 --- a/src/features/agent-app/manifest/parseManifest.ts +++ b/src/features/agent-app/manifest/parseManifest.ts @@ -1,5 +1,15 @@ import type { AppManifest } from "../types"; +export interface ManifestValueLayerField { + source: string; + target: string; +} + +export interface MergeLayeredManifestOptions { + arrayFields?: readonly string[]; + valueFields?: readonly ManifestValueLayerField[]; +} + export class AgentAppManifestError extends Error { constructor(message: string) { super(message); @@ -7,6 +17,22 @@ export class AgentAppManifestError extends Error { } } +const DEFAULT_ARRAY_LAYER_FIELDS = ["entries", "permissions"] as const; +const DEFAULT_VALUE_LAYER_FIELDS: readonly ManifestValueLayerField[] = [ + { source: "capabilities", target: "capabilityConfig" }, + { source: "errors", target: "errors" }, + { source: "i18n", target: "i18n" }, + { source: "signature", target: "signature" }, + { source: "agentRuntime", target: "agentRuntime" }, + { source: "requirements", target: "requirements" }, + { source: "boundary", target: "boundary" }, + { source: "boundaries", target: "boundary" }, + { source: "integrations", target: "integrations" }, + { source: "operations", target: "operations" }, + { source: "readiness", target: "readiness" }, + { source: "health", target: "health" }, +]; + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } @@ -18,28 +44,109 @@ function isString(value: unknown): value is string { function assertString(record: Record, key: string): string { const value = record[key]; if (!isString(value)) { - throw new AgentAppManifestError(`Agent App manifest missing string field: ${key}`); + throw new AgentAppManifestError( + `Agent App manifest missing string field: ${key}`, + ); } return value; } -export function parseManifest(input: unknown): AppManifest { +function readManifestInput(input: unknown): Record { const raw = typeof input === "string" ? JSON.parse(input) : input; if (!isRecord(raw)) { throw new AgentAppManifestError("Agent App manifest must be an object"); } + return raw; +} + +function layeredItemKey(value: unknown): string | undefined { + if (!isRecord(value)) { + return undefined; + } + const key = value.key ?? value.id; + return isString(key) ? key : undefined; +} + +function mergeNamedArrayLayer( + current: unknown, + overlay: readonly unknown[], +): unknown[] { + const merged = Array.isArray(current) ? [...current] : []; + + overlay.forEach((overlayItem) => { + const overlayKey = layeredItemKey(overlayItem); + if (!overlayKey) { + merged.push(overlayItem); + return; + } + + const existingIndex = merged.findIndex( + (item) => layeredItemKey(item) === overlayKey, + ); + if (existingIndex === -1) { + merged.push(overlayItem); + return; + } + + const existingItem = merged[existingIndex]; + merged[existingIndex] = + isRecord(existingItem) && isRecord(overlayItem) + ? { ...existingItem, ...overlayItem } + : overlayItem; + }); + + return merged; +} + +export function mergeLayeredManifest( + input: unknown, + layers: readonly unknown[], + options: MergeLayeredManifestOptions = {}, +): AppManifest { + const manifest: Record = { ...readManifestInput(input) }; + const arrayFields = options.arrayFields ?? DEFAULT_ARRAY_LAYER_FIELDS; + const valueFields = options.valueFields ?? DEFAULT_VALUE_LAYER_FIELDS; + + layers.forEach((layer) => { + if (!isRecord(layer)) { + return; + } + + arrayFields.forEach((field) => { + const items = layer[field]; + if (Array.isArray(items)) { + manifest[field] = mergeNamedArrayLayer(manifest[field], items); + } + }); + + valueFields.forEach(({ source, target }) => { + if (layer[source] !== undefined) { + manifest[target] = layer[source]; + } + }); + }); + + return parseManifest(manifest); +} + +export function parseManifest(input: unknown): AppManifest { + const raw = readManifestInput(input); assertString(raw, "manifestVersion"); assertString(raw, "name"); assertString(raw, "version"); if (!Array.isArray(raw.entries) || raw.entries.length === 0) { - throw new AgentAppManifestError("Agent App manifest must declare at least one entry"); + throw new AgentAppManifestError( + "Agent App manifest must declare at least one entry", + ); } raw.entries.forEach((entry, index) => { if (!isRecord(entry)) { - throw new AgentAppManifestError(`Agent App entry ${index} must be an object`); + throw new AgentAppManifestError( + `Agent App entry ${index} must be an object`, + ); } assertString(entry, "key"); assertString(entry, "kind"); diff --git a/src/features/agent-app/readiness/checkReadiness.test.ts b/src/features/agent-app/readiness/checkReadiness.test.ts index 26cf03db6..ca8f3ae94 100644 --- a/src/features/agent-app/readiness/checkReadiness.test.ts +++ b/src/features/agent-app/readiness/checkReadiness.test.ts @@ -123,6 +123,39 @@ describe("Agent App readiness P0", () => { expect(readiness.blockers).toHaveLength(0); }); + it("应接受 v0.7 manifest 作为当前 Host 标准,不再按运行时版本阻断", () => { + const rawManifest = parseManifest({ + manifestVersion: "0.7.0", + name: "content-factory-app", + version: "0.7.0", + requires: { + sdk: "@lime/app-sdk@^0.7.0", + capabilities: ["lime.capabilities"], + }, + entries: [{ key: "dashboard", kind: "page" }], + requirements: { + requirements: [{ id: "CF-R001", text: "生成内容草稿" }], + }, + boundary: { + boundaries: [ + { + requirementId: "CF-R001", + planes: { host: { requires: ["lime.agent"] } }, + }, + ], + }, + }); + const manifest = normalizeManifest(rawManifest); + const identity = buildPackageIdentity({ manifest: rawManifest }); + const projection = projectApp({ manifest, identity }); + const readiness = checkReadiness({ manifest, projection }); + + expect(manifest.manifestVersion).toBe("0.7"); + expect(readiness.blockers.map((issue) => issue.code)).not.toContain( + "MANIFEST_VERSION_UNSUPPORTED", + ); + }); + it("package verification mismatch 应产生 blocker,不能只作为 warning 继续启用", () => { const { manifest, projection } = buildProjection(); const profile: HostCapabilityProfile = { diff --git a/src/features/agent-app/readiness/checkReadiness.ts b/src/features/agent-app/readiness/checkReadiness.ts index 2d87d851f..9277624de 100644 --- a/src/features/agent-app/readiness/checkReadiness.ts +++ b/src/features/agent-app/readiness/checkReadiness.ts @@ -13,24 +13,41 @@ import type { ReadinessResult, ReadinessStatus, } from "../types"; -import { p0HostCapabilityProfile } from "./hostCapabilityProfile"; +import { + compatibleAgentAppStandardVersions, + p0HostCapabilityProfile, +} from "./hostCapabilityProfile"; function supportsManifestRuntime(manifest: NormalizedAppManifest): boolean { - return manifest.manifestVersion === "0.2" || manifest.manifestVersion === "0.3"; + return ( + manifest.manifestVersion === "0.2" || + manifest.manifestVersion === "0.3" || + compatibleAgentAppStandardVersions.includes(manifest.manifestVersion) + ); } function isRuntimeTargetSupported( manifest: NormalizedAppManifest, profile: HostCapabilityProfile, ): boolean { - return manifest.runtimeTargets.some((target) => profile.runtimeTargets.includes(target)); + return manifest.runtimeTargets.some((target) => + profile.runtimeTargets.includes(target), + ); } function normalizeRange(range: string): string { - return range.trim() || "*"; + const normalized = range.trim(); + if (!normalized) { + return "*"; + } + const sdkRange = normalized.match(/^@lime\/app-sdk@(.+)$/); + return sdkRange?.[1] ?? normalized; } -function supportsRequestedRange(hostVersion: string, requestedRange: string): boolean { +function supportsRequestedRange( + hostVersion: string, + requestedRange: string, +): boolean { const normalized = normalizeRange(requestedRange); if (normalized === "*") { return true; @@ -40,16 +57,21 @@ function supportsRequestedRange(hostVersion: string, requestedRange: string): bo const hostMinor = hostVersion.match(/^(\d+)\.(\d+)/); return Boolean( requestedMinor && - hostMinor && - requestedMinor[1] === hostMinor[1] && - requestedMinor[2] === hostMinor[2], + hostMinor && + requestedMinor[1] === hostMinor[1] && + requestedMinor[2] === hostMinor[2], ); } if (normalized.includes(hostVersion)) { return true; } if (normalized.includes(">=0.3.0") && normalized.includes("<1.0.0")) { - return hostVersion.startsWith("0.3."); + return ( + hostVersion.startsWith("0.3.") || + hostVersion.startsWith("0.5.") || + hostVersion.startsWith("0.6.") || + hostVersion.startsWith("0.7.") + ); } if (normalized.includes(">=0.2.0") && normalized.includes("<1.0.0")) { return hostVersion.startsWith("0.2.") || hostVersion.startsWith("0.3."); @@ -91,7 +113,9 @@ function issueForCapability( ): ReadinessIssue | null { if (!support.supported) { return { - code: support.hostVersion ? "CAPABILITY_VERSION_UNSUPPORTED" : "CAPABILITY_MISSING", + code: support.hostVersion + ? "CAPABILITY_VERSION_UNSUPPORTED" + : "CAPABILITY_MISSING", severity: requirement.required ? "blocker" : "warning", message: `${requirement.capability} is not available for ${requirement.requestedRange}.`, capability: requirement.capability, @@ -125,7 +149,9 @@ function entryStatus(issues: ReadinessIssue[]): ReadinessStatus { return "ready"; } -function issueFromCloudTool(tool: CloudBootstrapToolAvailability): ReadinessIssue | null { +function issueFromCloudTool( + tool: CloudBootstrapToolAvailability, +): ReadinessIssue | null { if (tool.status === "available") { return null; } @@ -174,7 +200,10 @@ function collectCloudReadinessIssues(cloud?: CloudBootstrapApp): { severity: "blocker", message: `Cloud license state is ${cloud.licenseState}.`, }); - } else if (cloud.licenseState === "trial" || cloud.licenseState === "unknown") { + } else if ( + cloud.licenseState === "trial" || + cloud.licenseState === "unknown" + ) { warnings.push({ code: "CLOUD_LICENSE_UNAVAILABLE", severity: "warning", @@ -198,7 +227,8 @@ function collectCloudReadinessIssues(cloud?: CloudBootstrapApp): { blockers.push({ code: "CLOUD_POLICY_UNSUPPORTED", severity: "blocker", - message: "Cloud policy defaults cannot enable server-assisted runtime locally.", + message: + "Cloud policy defaults cannot enable server-assisted runtime locally.", }); } @@ -264,7 +294,10 @@ function collectProjectionSetupIssues( blockers: ReadinessIssue[]; warnings: ReadinessIssue[]; } { - const result = { blockers: [] as ReadinessIssue[], warnings: [] as ReadinessIssue[] }; + const result = { + blockers: [] as ReadinessIssue[], + warnings: [] as ReadinessIssue[], + }; projection.knowledgeBindings.forEach((binding) => { pushSetupIssueWhenMissing(result, { @@ -377,7 +410,10 @@ function collectProjectionSetupIssues( return result; } -function cloudEntryIssue(entryKey: string, cloud?: CloudBootstrapApp): ReadinessIssue | null { +function cloudEntryIssue( + entryKey: string, + cloud?: CloudBootstrapApp, +): ReadinessIssue | null { if (!cloud) { return null; } @@ -389,7 +425,10 @@ function cloudEntryIssue(entryKey: string, cloud?: CloudBootstrapApp): Readiness entryKey, }; } - if (cloud.defaultEntries.length > 0 && !cloud.defaultEntries.includes(entryKey)) { + if ( + cloud.defaultEntries.length > 0 && + !cloud.defaultEntries.includes(entryKey) + ) { return { code: "CLOUD_ENTRY_NOT_ENABLED", severity: "warning", @@ -433,7 +472,10 @@ export function checkReadiness(params: { const blockers: ReadinessIssue[] = []; const warnings: ReadinessIssue[] = []; const cloudIssues = collectCloudReadinessIssues(params.cloud); - const setupIssues = collectProjectionSetupIssues(params.projection, params.setup); + const setupIssues = collectProjectionSetupIssues( + params.projection, + params.setup, + ); blockers.push(...cloudIssues.blockers); warnings.push(...cloudIssues.warnings); blockers.push(...setupIssues.blockers); @@ -464,11 +506,15 @@ export function checkReadiness(params: { warnings.push({ code: "STORAGE_DECLARED_BUT_DISABLED", severity: "warning", - message: "Storage namespace is declared, but P0 does not create local App storage.", + message: + "Storage namespace is declared, but P0 does not create local App storage.", }); } - if (params.projection.runtimePackage.hasUiBundle && !profile.featureFlags.uiRuntimeEnabled) { + if ( + params.projection.runtimePackage.hasUiBundle && + !profile.featureFlags.uiRuntimeEnabled + ) { warnings.push({ code: "UI_RUNTIME_DISABLED", severity: "warning", @@ -483,16 +529,20 @@ export function checkReadiness(params: { warnings.push({ code: "WORKER_RUNTIME_DISABLED", severity: "warning", - message: "Worker bundle is declared, but P0 keeps worker runtime disabled.", + message: + "Worker bundle is declared, but P0 keeps worker runtime disabled.", }); } - const capabilitySupports = params.projection.requiredCapabilities.map((requirement) => - capabilitySupport(requirement, profile), + const capabilitySupports = params.projection.requiredCapabilities.map( + (requirement) => capabilitySupport(requirement, profile), ); capabilitySupports.forEach((support, index) => { - const issue = issueForCapability(params.projection.requiredCapabilities[index], support); + const issue = issueForCapability( + params.projection.requiredCapabilities[index], + support, + ); if (!issue) { return; } @@ -503,21 +553,28 @@ export function checkReadiness(params: { } }); - const entryReadiness: EntryReadiness[] = params.projection.entries.map((entry) => { - const entryIssues = entry.requiredCapabilities - .map((requirement) => issueForCapability(requirement, capabilitySupport(requirement, profile))) - .filter((issue): issue is ReadinessIssue => Boolean(issue)); - const cloudIssue = cloudEntryIssue(entry.key, params.cloud); - if (cloudIssue) { - entryIssues.push(cloudIssue); - } + const entryReadiness: EntryReadiness[] = params.projection.entries.map( + (entry) => { + const entryIssues = entry.requiredCapabilities + .map((requirement) => + issueForCapability( + requirement, + capabilitySupport(requirement, profile), + ), + ) + .filter((issue): issue is ReadinessIssue => Boolean(issue)); + const cloudIssue = cloudEntryIssue(entry.key, params.cloud); + if (cloudIssue) { + entryIssues.push(cloudIssue); + } - return { - entryKey: entry.key, - status: entryStatus(entryIssues), - issues: entryIssues, - }; - }); + return { + entryKey: entry.key, + status: entryStatus(entryIssues), + issues: entryIssues, + }; + }, + ); const status: ReadinessStatus = blockers.length ? "blocked" @@ -534,10 +591,12 @@ export function checkReadiness(params: { blockers, warnings, supportedCapabilities: capabilitySupports, - missingCapabilities: params.projection.requiredCapabilities.filter((requirement, index) => { - const support = capabilitySupports[index]; - return !support.supported || !support.enabled; - }), + missingCapabilities: params.projection.requiredCapabilities.filter( + (requirement, index) => { + const support = capabilitySupports[index]; + return !support.supported || !support.enabled; + }, + ), entryReadiness, }; } diff --git a/src/features/agent-app/readiness/hostCapabilityProfile.ts b/src/features/agent-app/readiness/hostCapabilityProfile.ts index f7d7d56db..ea305501f 100644 --- a/src/features/agent-app/readiness/hostCapabilityProfile.ts +++ b/src/features/agent-app/readiness/hostCapabilityProfile.ts @@ -1,19 +1,18 @@ import { defaultAgentAppHostFlags } from "../featureFlag"; import type { HostCapabilityProfile } from "../types"; +import { buildLimeCapabilityProfileEntriesForMode } from "../sdk/capabilityCatalog"; + +export const currentAgentAppHostRuntimeVersion = "0.7.0"; +export const currentAgentAppStandardVersion = "0.7"; +export const compatibleAgentAppStandardVersions = ["0.5", "0.6", "0.7"]; export const p0HostCapabilityProfile: HostCapabilityProfile = { - appRuntimeVersion: "0.3.0", - runtimeTargets: ["local"], - capabilities: { - "lime.ui": { version: "0.3.0", enabled: false, implementation: "none" }, - "lime.storage": { version: "0.3.0", enabled: false, implementation: "none" }, - "lime.files": { version: "0.3.0", enabled: false, implementation: "none" }, - "lime.agent": { version: "0.3.0", enabled: false, implementation: "none" }, - "lime.knowledge": { version: "0.3.0", enabled: false, implementation: "none" }, - "lime.tools": { version: "0.3.0", enabled: false, implementation: "none" }, - "lime.artifacts": { version: "0.3.0", enabled: false, implementation: "none" }, - "lime.evidence": { version: "0.3.0", enabled: false, implementation: "none" }, - "lime.workflow": { version: "0.3.0", enabled: false, implementation: "none" }, + appRuntimeVersion: currentAgentAppHostRuntimeVersion, + standardVersions: { + current: currentAgentAppStandardVersion, + compatible: [...compatibleAgentAppStandardVersions], }, + runtimeTargets: ["local"], + capabilities: buildLimeCapabilityProfileEntriesForMode("base"), featureFlags: defaultAgentAppHostFlags, }; diff --git a/src/features/agent-app/runtime/agentRuntimeCapabilityHost.test.ts b/src/features/agent-app/runtime/agentRuntimeCapabilityHost.test.ts index f4ce72b2d..e2cac5aa4 100644 --- a/src/features/agent-app/runtime/agentRuntimeCapabilityHost.test.ts +++ b/src/features/agent-app/runtime/agentRuntimeCapabilityHost.test.ts @@ -123,6 +123,9 @@ describe("AgentRuntimeCapabilityHost", () => { runStartHooks: false, }); const snapshot = await sdk.agent.getTask(started.taskId); + const listedAfterSnapshot = host + .getTasks({ appId: "content-factory-app" }) + .find((task) => task.taskId === started.taskId); const stream = await sdk.agent.streamTask(started.taskId); const hostResponse = await sdk.agent.submitHostResponse({ taskId: started.taskId, @@ -184,6 +187,22 @@ describe("AgentRuntimeCapabilityHost", () => { }), ]), }); + expect(listedAfterSnapshot?.events).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "evidence:recorded", + refs: ["evidence-1"], + }), + expect.objectContaining({ + type: "artifact:created", + refs: [".lime/artifacts/content-batch.json"], + }), + ]), + ); + expect(listedAfterSnapshot?.runtimeProcess?.usage).toMatchObject({ + estimated: true, + source: "agent_app_runtime_process_estimate", + }); expect(stream).toEqual( expect.arrayContaining([ expect.objectContaining({ type: "task:missingContextRequested" }), @@ -374,6 +393,171 @@ describe("AgentRuntimeCapabilityHost", () => { }); }); + it("在主 App 侧封装 Claw 式运行过程,包含模型、Token、费用和 Skill", async () => { + const api = { + startTask: vi.fn(async (request) => ({ + appId: request.appId, + entryKey: request.entryKey, + taskId: "agent-app-task-process", + traceId: "agent-app-trace-process", + taskKind: request.taskKind, + sessionId: "session-process", + turnId: "turn-process", + eventName: `agent_app_runtime:${request.appId}:agent-app-task-process`, + status: "accepted" as const, + submittedAt: "2026-05-15T00:00:00.000Z", + })), + getTask: vi.fn(async (request) => ({ + appId: request.appId, + taskId: request.taskId, + sessionId: request.sessionId, + status: "thread_read_available" as const, + taskStatus: "completed", + taskEvents: [ + { + id: "runtime:routing:decision", + eventType: "task:progress", + status: "routing", + message: "模型路由已确定:openai/gpt-4.1", + payload: { + runtimeEvent: { + type: "routing_decision_made", + routing_decision: { + candidate_count: 2, + selected_provider: "openai", + selected_model: "gpt-4.1", + }, + }, + }, + }, + { + id: "runtime:thinking", + eventType: "task:progress", + status: "thinking", + message: "先分析内容目标", + payload: { + streamKind: "thinking_delta", + delta: "先分析内容目标", + runtimeEvent: { type: "thinking_delta", text: "先分析内容目标" }, + }, + }, + { + id: "runtime:text", + eventType: "task:partialArtifact", + status: "streaming", + message: "第一段输出", + payload: { + streamKind: "assistant_text_delta", + delta: "第一段输出", + runtimeEvent: { type: "text_delta", text: "第一段输出" }, + }, + }, + { + id: "runtime:skill", + eventType: "task:toolCall", + status: "completed", + message: "工具 Skill completed", + toolName: "Skill", + payload: { + runtimeEvent: { + type: "tool_end", + result: { output: "完成", metadata: { command_name: "knowledge-builder" } }, + }, + }, + }, + { + id: "runtime:cost", + eventType: "task:runtimeEvent", + status: "recorded", + message: "消耗已记录", + payload: { + runtimeEvent: { + type: "cost_recorded", + cost_state: { + estimated_total_cost: 0.0032, + currency: "USD", + }, + }, + }, + }, + { + id: "runtime:done", + eventType: "task:completed", + status: "completed", + message: "AgentRuntime 本轮输出已结束", + payload: { + runtimeEvent: { + type: "final_done", + usage: { input_tokens: 1200, output_tokens: 340 }, + }, + }, + }, + ], + threadRead: { + session_id: request.sessionId, + profile_status: "completed", + }, + })), + cancelTask: vi.fn(async (request) => ({ + appId: request.appId, + taskId: request.taskId, + sessionId: request.sessionId, + cancelled: true, + status: "cancelled" as const, + })), + submitHostResponse: vi.fn(async (request) => ({ + appId: request.appId, + taskId: request.taskId, + status: "submitted" as const, + })), + }; + const host = new AgentRuntimeCapabilityHost({ + delegate: buildDelegateHost(), + appId: "content-factory-app", + appVersion: "0.3.0", + packageHash: "package-hash-1", + manifestHash: "manifest-hash-1", + workspaceIdResolver: async () => "workspace-1", + api, + now: () => "2026-05-15T00:00:04.000Z", + }); + const sdk = host.createSdkContext("dashboard"); + const started = await sdk.agent.startTask({ + title: "生成内容批次", + taskKind: "content.copy.generate", + input: { projectId: "project-1" }, + expectedOutput: { + artifactKind: "content_batch", + requiredSkills: [ + { skill: "knowledge-builder", required: true }, + { skill: "content-reviewer", required: true }, + ], + }, + }); + + const snapshot = await sdk.agent.getTask(started.taskId); + + expect(snapshot?.runtimeProcess).toMatchObject({ + terminal: true, + collapsedByDefault: true, + model: { provider: "openai", model: "gpt-4.1", label: "openai/gpt-4.1" }, + usage: { inputTokens: 1200, outputTokens: 340, totalTokens: 1540 }, + cost: { estimatedTotalCost: 0.0032, currency: "USD" }, + skillNames: expect.arrayContaining(["knowledge-builder", "content-reviewer"]), + invokedSkillNames: ["knowledge-builder"], + streamText: "第一段输出", + thinkingText: "先分析内容目标", + }); + expect(snapshot?.process).toBe(snapshot?.runtimeProcess); + expect(snapshot?.runtimeProcess?.timeline).toEqual( + expect.arrayContaining([ + expect.objectContaining({ kind: "routing", title: "模型路由" }), + expect.objectContaining({ kind: "skill", title: "Skill · knowledge-builder" }), + expect.objectContaining({ kind: "metrics", title: "消耗统计" }), + ]), + ); + }); + it("从 threadRead artifacts 补投 artifact:created payload,保证 Host Bridge 可 replay 最终产物", async () => { const api = { startTask: vi.fn(async (request) => ({ @@ -471,6 +655,13 @@ describe("AgentRuntimeCapabilityHost", () => { }), }), }), + expect.objectContaining({ + type: "evidence:recorded", + refs: ["evidence:.lime/artifacts/content-batch.json"], + payload: expect.objectContaining({ + source: "agent_runtime_artifact_replay", + }), + }), ]), ); }); diff --git a/src/features/agent-app/runtime/agentRuntimeCapabilityHost.ts b/src/features/agent-app/runtime/agentRuntimeCapabilityHost.ts index 8a34cea6f..9fd0cb1f4 100644 --- a/src/features/agent-app/runtime/agentRuntimeCapabilityHost.ts +++ b/src/features/agent-app/runtime/agentRuntimeCapabilityHost.ts @@ -36,6 +36,7 @@ import type { AgentAppUninstallResult, AppCleanupPlan, } from "../types"; +import { buildAgentRuntimeProcessView } from "./agentRuntimeProcess"; export interface AgentAppRuntimeCapabilityApi { startTask( @@ -80,6 +81,7 @@ interface RuntimeTaskState { request: AgentAppTaskRequest; retryOfTaskId?: string; retryAttempt?: number; + latestSnapshot?: AgentAppRuntimeTaskSnapshot; } const RUNTIME_TASK_STORAGE_PREFIX = "agent-runtime/tasks/"; @@ -254,6 +256,96 @@ function readThreadReadArtifacts(threadRead: unknown): Record[] return threadRead.artifacts.filter(isRecord); } +function parseJsonObjectFromMarkdown(value: string): Record | null { + const trimmed = value.trim(); + const candidate = trimmed.startsWith("```") + ? trimmed + .split("\n") + .slice(1) + .join("\n") + .replace(/```\s*$/m, "") + .trim() + : trimmed; + try { + const parsed = JSON.parse(candidate); + return isRecord(parsed) ? parsed : null; + } catch { + const start = candidate.indexOf("{"); + const end = candidate.lastIndexOf("}"); + if (start < 0 || end <= start) { + return null; + } + try { + const parsed = JSON.parse(candidate.slice(start, end + 1)); + return isRecord(parsed) ? parsed : null; + } catch { + return null; + } + } +} + +function hasContentFactoryWorkspacePatchFields(value: Record): boolean { + return [ + "workspace", + "project", + "sceneTable", + "contentBatch", + "scripts", + "imagePrompts", + "assetPack", + "projectKnowledge", + ].some((key) => isRecord(value[key]) || Array.isArray(value[key])); +} + +function extractContentFactoryWorkspacePatch( + value: unknown, +): Record | undefined { + if (!isRecord(value)) { + return undefined; + } + for (const key of ["contentFactoryWorkspacePatch", "workspacePatch"]) { + const patch = value[key]; + if (isRecord(patch)) { + return patch; + } + } + const kind = readRecordString(value, "kind") ?? readRecordString(value, "artifactKind"); + if ( + kind === "content_factory.workspace_patch" || + kind === "contentFactoryWorkspacePatch" || + kind === "workspacePatch" || + hasContentFactoryWorkspacePatchFields(value) + ) { + return value; + } + return undefined; +} + +function extractWorkspacePatchFromArtifactDocument( + artifactDocument: unknown, +): Record | undefined { + if (!isRecord(artifactDocument) || !Array.isArray(artifactDocument.blocks)) { + return undefined; + } + for (const block of artifactDocument.blocks) { + if (!isRecord(block)) { + continue; + } + const content = + typeof block.content === "string" + ? block.content + : typeof block.markdown === "string" + ? block.markdown + : ""; + const parsed = content ? parseJsonObjectFromMarkdown(content) : null; + const patch = extractContentFactoryWorkspacePatch(parsed); + if (patch) { + return patch; + } + } + return undefined; +} + function buildRuntimeArtifactReplayEvents( state: RuntimeTaskState, threadRead: unknown, @@ -283,29 +375,50 @@ function buildRuntimeArtifactReplayEvents( const workspacePatch = isRecord(metadata.workspacePatch) || isRecord(metadata.contentFactoryWorkspacePatch) ? metadata.workspacePatch ?? metadata.contentFactoryWorkspacePatch - : undefined; + : extractWorkspacePatchFromArtifactDocument(artifactDocument); + const at = + readRecordString(artifact, "completed_at") ?? + readRecordString(artifact, "updated_at") ?? + readRecordString(artifact, "created_at") ?? + state.startedAt; + const artifactEvent: AgentAppTaskStreamEvent = { + eventId: `${state.taskId}:artifact:${artifactRef}`, + taskId: state.taskId, + traceId: state.traceId, + type: "artifact:created", + status: "succeeded", + at, + message: + readRecordString(artifact, "title") ?? + readRecordString(artifact, "artifact_type") ?? + "Artifact 已创建", + refs: [artifactRef], + payload: { + artifact, + artifactDocument, + workspacePatch, + contentFactoryWorkspacePatch: workspacePatch, + }, + }; + if (!workspacePatch) { + return [artifactEvent]; + } return [ + artifactEvent, { - eventId: `${state.taskId}:artifact:${artifactRef}`, + eventId: `${state.taskId}:evidence:${artifactRef}`, taskId: state.taskId, traceId: state.traceId, - type: "artifact:created" as const, - status: "succeeded" as const, - at: - readRecordString(artifact, "completed_at") ?? - readRecordString(artifact, "updated_at") ?? - readRecordString(artifact, "created_at") ?? - state.startedAt, - message: - readRecordString(artifact, "title") ?? - readRecordString(artifact, "artifact_type") ?? - "Artifact 已创建", - refs: [artifactRef], + type: "evidence:recorded", + status: "succeeded", + at, + message: "内容工厂 workspace patch evidence 已记录", + refs: [`evidence:${artifactRef}`], payload: { - artifact, - artifactDocument, + artifactRef, workspacePatch, contentFactoryWorkspacePatch: workspacePatch, + source: "agent_runtime_artifact_replay", }, }, ]; @@ -316,28 +429,41 @@ function buildTaskRecord( state: RuntimeTaskState, snapshot?: AgentAppRuntimeTaskSnapshot, ): AgentAppTaskRecord { - const runtimeEvents = snapshot?.taskEvents.length - ? snapshot.taskEvents.map((event, index) => + const effectiveSnapshot = snapshot ?? state.latestSnapshot; + const runtimeEvents = effectiveSnapshot?.taskEvents.length + ? effectiveSnapshot.taskEvents.map((event, index) => mapRuntimeEvent(state, event, index), ) : [buildStartEvent(state)]; - const events = snapshot + const events = effectiveSnapshot ? [ ...runtimeEvents, ...buildRuntimeArtifactReplayEvents( state, - snapshot.threadRead, + effectiveSnapshot.threadRead, runtimeEvents, ), ] : runtimeEvents; - const status = snapshot - ? mapRuntimeTaskStatus(snapshot.taskStatus) + const status = effectiveSnapshot + ? mapRuntimeTaskStatus(effectiveSnapshot.taskStatus) : "running"; const finishedAt = status === "succeeded" || status === "failed" || status === "cancelled" ? events[events.length - 1]?.at : undefined; + const runtimeProcess = buildAgentRuntimeProcessView({ + events, + task: { + status, + taskStatus: effectiveSnapshot?.taskStatus, + input: state.request.input, + expectedOutput: state.request.expectedOutput, + }, + snapshot: effectiveSnapshot, + expectedOutput: state.request.expectedOutput, + lastInput: state.request.input, + }); return { taskId: state.taskId, @@ -366,7 +492,7 @@ function buildTaskRecord( startedAt: state.startedAt, finishedAt, cancelledAt: status === "cancelled" ? finishedAt : undefined, - result: snapshot?.threadRead, + result: effectiveSnapshot?.threadRead, trace: events .filter((event) => event.message) .map((event) => ({ @@ -374,6 +500,8 @@ function buildTaskRecord( message: event.message ?? "", })), events, + runtimeProcess, + process: runtimeProcess, provenance: { sourceKind: "agent_app", appId: state.appId, @@ -583,6 +711,8 @@ export class AgentRuntimeCapabilityHost implements CapabilityHost { taskId: state.taskId, sessionId: state.sessionId, }); + state.latestSnapshot = snapshot; + this.tasks.set(state.taskId, state); return buildTaskRecord(state, snapshot); } diff --git a/src/features/agent-app/runtime/agentRuntimeProcess.test.ts b/src/features/agent-app/runtime/agentRuntimeProcess.test.ts new file mode 100644 index 000000000..2c8c05872 --- /dev/null +++ b/src/features/agent-app/runtime/agentRuntimeProcess.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from "vitest"; + +import { buildAgentRuntimeProcessView } from "./agentRuntimeProcess"; + +describe("buildAgentRuntimeProcessView", () => { + it("保留完整运行过程,不在数据层截断到最后 16 条", () => { + const events = Array.from({ length: 20 }, (_, index) => ({ + eventType: "task:progress", + message: `步骤${index + 1}`, + })); + + const process = buildAgentRuntimeProcessView({ events }); + + expect(process.timeline).toHaveLength(20); + expect(process.timeline[0]).toMatchObject({ message: "步骤1" }); + expect(process.timeline.at(-1)).toMatchObject({ message: "步骤20" }); + }); + + it("保留每段流式输出过程,折叠只交给 UI 层处理", () => { + const events = ["第一段", "第二段", "第三段"].map((delta) => ({ + eventType: "task:partialArtifact", + status: "streaming", + message: delta, + payload: { + streamKind: "assistant_text_delta", + delta, + runtimeEvent: { type: "text_delta", text: delta }, + }, + })); + + const process = buildAgentRuntimeProcessView({ events }); + + expect(process.streamText).toBe("第一段第二段第三段"); + expect(process.timeline.map((item) => item.message)).toEqual([ + "第一段", + "第二段", + "第三段", + ]); + }); + + it("从 Skill 工具参数对象中识别真实调用的业务 Skill", () => { + const process = buildAgentRuntimeProcessView({ + events: [ + { + eventType: "task:toolCall", + status: "completed", + message: "工具 Skill completed", + toolName: "Skill", + payload: { + runtimeEvent: { + type: "tool_start", + toolName: "Skill", + arguments: { + skill: "knowledge-builder", + args: { projectId: "project-1" }, + }, + }, + }, + }, + ], + }); + + expect(process.invokedSkillNames).toEqual(["knowledge-builder"]); + expect(process.timeline[0]).toMatchObject({ + kind: "skill", + title: "Skill · knowledge-builder", + }); + }); + + it("从 threadRead.model_routing 和 artifact 事件投影完成态运行事实", () => { + const process = buildAgentRuntimeProcessView({ + task: { + taskStatus: "completed", + input: { + projectId: "project-1", + material: "这是一段用于内容工厂知识库整理的项目资料。", + }, + }, + snapshot: { + taskStatus: "completed", + threadRead: { + model_routing: { + selectedProvider: "deepseek", + selectedModel: "deepseek-v4-flash", + }, + cost_state: { + estimatedCostClass: "medium", + status: "estimated", + }, + }, + }, + events: [ + { + eventType: "task:toolCall", + status: "completed", + message: "工具 Skill completed", + toolName: "Skill", + payload: { + tool_name: "Skill", + }, + }, + { + eventType: "artifact:created", + status: "created", + message: "内容工厂 workspace patch 已创建", + payload: { + contentFactoryWorkspacePatch: { + kind: "content_factory.workspace_patch", + projectId: "project-1", + }, + }, + }, + ], + }); + + expect(process.model).toMatchObject({ + provider: "deepseek", + model: "deepseek-v4-flash", + label: "deepseek/deepseek-v4-flash", + }); + expect(process.usage).toMatchObject({ + estimated: true, + source: "agent_app_runtime_process_estimate", + }); + expect(process.cost).toMatchObject({ + estimatedCostClass: "medium", + status: "estimated", + }); + expect(process.artifactCount).toBe(1); + expect(process.terminal).toBe(true); + }); +}); diff --git a/src/features/agent-app/runtime/agentRuntimeProcess.ts b/src/features/agent-app/runtime/agentRuntimeProcess.ts new file mode 100644 index 000000000..5df84f507 --- /dev/null +++ b/src/features/agent-app/runtime/agentRuntimeProcess.ts @@ -0,0 +1,1042 @@ +import type { + AgentAppRuntimeProcessCost, + AgentAppRuntimeProcessModel, + AgentAppRuntimeProcessTimelineItem, + AgentAppRuntimeProcessUsage, + AgentAppRuntimeProcessView, +} from "../types"; + +export interface BuildAgentRuntimeProcessViewOptions { + events?: unknown[]; + task?: unknown; + snapshot?: unknown; + expectedOutput?: unknown; + lastInput?: unknown; + contract?: unknown; +} + +export function buildAgentRuntimeProcessView({ + events = [], + task = null, + snapshot = null, + expectedOutput = null, + lastInput = null, + contract = null, +}: BuildAgentRuntimeProcessViewOptions = {}): AgentAppRuntimeProcessView { + const safeEvents = Array.isArray(events) ? events : []; + const timeline = buildTimelineItems(safeEvents); + const invokedSkillNames = uniqueTextValues( + safeEvents.map(extractSkillNameFromEvent), + ); + const taskRecord = isRecord(task) ? task : null; + const resolvedExpectedOutput = + expectedOutput ?? (taskRecord ? taskRecord.expectedOutput : undefined); + const resolvedLastInput = lastInput ?? (taskRecord ? taskRecord.input : undefined); + const skillNames = uniqueTextValues([ + ...collectRequiredSkillNames(task), + ...collectRequiredSkillNames(resolvedLastInput), + ...collectRequiredSkillNames(resolvedExpectedOutput), + ...invokedSkillNames, + ]); + const usage = extractUsageFromProcess(safeEvents, snapshot, task); + const cost = extractCostFromProcess(safeEvents, snapshot, task); + const model = extractModelFromProcess(safeEvents, task, snapshot); + const terminal = isTerminalProcess({ + task, + snapshot, + events: safeEvents, + contract, + }); + + return { + timeline, + streamText: collectStream(safeEvents, "assistant_text"), + thinkingText: collectStream(safeEvents, "thinking"), + executionText: collectExecutionStream(safeEvents), + skillNames, + invokedSkillNames, + model, + usage, + cost, + terminal, + collapsedByDefault: terminal, + routingCount: timeline.filter((item) => item.kind === "routing").length, + executionCount: timeline.filter((item) => + ["skill", "tool", "execution"].includes(item.kind), + ).length, + artifactCount: timeline.filter((item) => item.kind === "artifact").length, + }; +} + +export function normalizeAgentRuntimeProcessTimelineItem( + event: unknown, +): AgentAppRuntimeProcessTimelineItem | null { + if (!isRecord(event)) { + return null; + } + + const stream = readStreamEvent(event); + if (stream?.text) { + const config = + { + thinking: ["thinking", "思考过程", "流式思考"], + assistant_text: ["output", "成稿流式输出", "生成中"], + tool_input: ["execution", "执行参数流", "输入中"], + tool_output: ["execution", "执行结果流", "输出中"], + }[stream.kind] ?? ["progress", "运行片段", "更新中"]; + return { + kind: config[0] as AgentAppRuntimeProcessTimelineItem["kind"], + title: stream.toolName ? `${config[1]} · ${stream.toolName}` : config[1], + statusText: config[2], + message: clipText(stream.text, 140), + detail: "", + meta: stream.streamKind ?? "", + collapseKey: `stream:${stream.kind}:${stream.toolName || "main"}`, + }; + } + + const runtimeEvent = readRuntimeEvent(event); + const runtimeType = readRuntimeEventType(event, runtimeEvent); + const eventType = String( + event.eventType ?? event.type ?? runtimeType ?? "", + ).toLowerCase(); + const message = String( + event.message ?? runtimeType ?? event.eventType ?? "任务进度已更新", + ).trim(); + const statusText = statusTextFor( + readString(event, "status") || readString(recordValue(event, "payload"), "status"), + ); + const toolName = extractToolNameFromEvent(event); + const skillName = extractSkillNameFromEvent(event); + const detail = extractEventDetail(event); + const runtimeSurface = `${eventType} ${runtimeType} ${message}`; + + if (message === "任务状态:idle") { + return { + kind: "progress", + title: "接收任务", + statusText: "已接收", + message: "AI 同事已接收任务,等待运行进度回写。", + detail, + }; + } + if (/routing|candidate|model_change/.test(runtimeType) || /routing/.test(message)) { + return { + kind: "routing", + title: "模型路由", + statusText: "已决策", + message: routingLabel(runtimeEvent) || message, + detail, + collapseKey: "runtime:routing", + }; + } + if (/cost_estimated|cost_recorded/.test(runtimeType)) { + const cost = extractCostState(runtimeEvent); + return { + kind: "metrics", + title: "消耗统计", + statusText: runtimeType === "cost_recorded" ? "已记录" : "预估中", + message: formatAgentRuntimeCostText(cost), + detail: "", + collapseKey: runtimeType, + }; + } + if (/task_profile|taskprofileresolved/.test(runtimeType)) { + return { + kind: "progress", + title: "任务识别", + statusText: "已识别", + message, + detail, + collapseKey: "runtime:task-profile", + }; + } + if (toolName || eventType.includes("tool")) { + const title = formatToolTitle(toolName, skillName); + return { + kind: title.startsWith("Skill ·") ? "skill" : "tool", + title, + statusText, + message: normalizeToolMessage(message, statusText), + detail, + meta: + readString(event, "toolId") || + readString(runtimeEvent, "tool_id") || + readString(runtimeEvent, "toolId"), + }; + } + if ( + eventType.includes("artifact") || + /artifact/.test(runtimeType) || + /artifact|workspacePatch|contentFactoryWorkspacePatch/i.test(message) + ) { + return { + kind: "artifact", + title: "产物回写", + statusText, + message: runtimeAssetLabel(event) || message, + detail, + }; + } + if (eventType.includes("evidence") || /evidence|verification/i.test(runtimeType)) { + return { + kind: "artifact", + title: "证据记录", + statusText, + message: evidenceLabel(event) || message, + detail, + }; + } + if (/missing|blocked|review|action_required|actionrequired/i.test(runtimeSurface)) { + return { + kind: "blocked", + title: "等待确认", + statusText, + message: normalizeConfirmationMessage(message), + detail, + }; + } + if (/failed|failure|error|incident|warning|cancel/.test(runtimeSurface.toLowerCase())) { + return { + kind: "warning", + title: "运行提醒", + statusText, + message, + detail, + }; + } + if (/completed|complete|done|final_done|turn_completed|turncompleted/.test(runtimeSurface.toLowerCase())) { + const usage = extractUsageObject(runtimeEvent); + return { + kind: "completed", + title: "回合完成", + statusText: "已完成", + message: usage + ? `AgentRuntime 本轮输出已结束,${formatAgentRuntimeUsageText(usage)}` + : message, + detail, + collapseKey: "runtime:done", + }; + } + if (/streamTask|getTask|agent-app-runtime/i.test(message)) { + return { + kind: "progress", + title: "同步运行事件", + statusText, + message: "正在同步 Lime 运行事件。", + detail, + }; + } + if (/running|started|queued/i.test(message)) { + return { + kind: "execution", + title: "执行阶段", + statusText, + message: "AI 同事正在执行本轮任务。", + detail, + }; + } + return { + kind: "progress", + title: "运行进度", + statusText, + message, + detail, + }; +} + +export function formatAgentRuntimeUsageText( + usage: AgentAppRuntimeProcessUsage | null | undefined, +): string { + if (!usage) { + return "Token 等待回写"; + } + const input = numberValue(usage.inputTokens ?? usage.input_tokens); + const output = numberValue(usage.outputTokens ?? usage.output_tokens); + const total = numberValue(usage.totalTokens ?? usage.total_tokens) || input + output; + if (!input && !output && !total) { + return "Token 等待回写"; + } + return `${formatNumber(total)} tokens(输入 ${formatNumber(input)} / 输出 ${formatNumber(output)})`; +} + +export function formatAgentRuntimeCostText( + cost: AgentAppRuntimeProcessCost | null | undefined, +): string { + if (!cost) { + return "费用等待回写"; + } + const total = optionalNumberValue( + cost.estimatedTotalCost ?? cost.estimated_total_cost ?? cost.totalCost ?? cost.total_cost, + ); + const costClass = stringValue( + cost.estimatedCostClass ?? cost.estimated_cost_class, + ); + const currency = stringValue(cost.currency) || "USD"; + if (total !== undefined) { + return `${currency} ${total.toFixed(total < 0.01 ? 4 : 2)}`; + } + if (costClass) { + return `预估等级:${costClass}`; + } + return "费用等待回写"; +} + +function buildTimelineItems(events: unknown[]): AgentAppRuntimeProcessTimelineItem[] { + const items: AgentAppRuntimeProcessTimelineItem[] = []; + const seen = new Set(); + for (const event of events) { + const item = normalizeAgentRuntimeProcessTimelineItem(event); + if (!item) { + continue; + } + const signature = [ + item.kind, + item.title, + item.statusText, + item.message, + item.detail, + item.meta, + item.collapseKey, + ].join("|"); + if (seen.has(signature)) { + continue; + } + seen.add(signature); + items.push(item); + } + return items; +} + +function collectStream(events: unknown[], targetKind: AgentRuntimeStreamKind): string { + return events + .map(readStreamEvent) + .filter((item): item is AgentRuntimeStreamEvent => + Boolean(item?.kind === targetKind && item.text), + ) + .map((item) => item.text) + .join(""); +} + +function collectExecutionStream(events: unknown[]): string { + return events + .map(readStreamEvent) + .filter((item): item is AgentRuntimeStreamEvent => + Boolean(item && ["tool_input", "tool_output"].includes(item.kind) && item.text), + ) + .map((item) => { + const label = item.kind === "tool_input" ? "输入" : "输出"; + return `${label}${item.toolName ? ` · ${item.toolName}` : ""}\n${item.text}`; + }) + .join("\n\n"); +} + +type AgentRuntimeStreamKind = + | "assistant_text" + | "thinking" + | "tool_input" + | "tool_output"; + +interface AgentRuntimeStreamEvent { + kind: AgentRuntimeStreamKind; + text: string; + toolName?: string; + streamKind?: string; +} + +function readStreamEvent(event: unknown): AgentRuntimeStreamEvent | null { + if (!isRecord(event)) { + return null; + } + const payload = recordValue(event, "payload"); + const runtimeEvent = readRuntimeEvent(event); + const streamKind = + readString(payload, "streamKind") || + readString(payload, "stream_kind") || + readString(event, "streamKind"); + const runtimeType = readRuntimeEventType(event, runtimeEvent); + const text = + readString(payload, "delta") || + readString(payload, "text") || + readString(runtimeEvent, "delta") || + readString(runtimeEvent, "text") || + readString(runtimeEvent, "arguments") || + readString(runtimeEvent, "accumulated_arguments") || + readString(runtimeEvent, "accumulatedArguments") || + (runtimeType === "text_delta_batch" ? readString(event, "message") : ""); + if (!text) { + return null; + } + const toolName = formatToolName( + extractToolNameFromEvent(event), + extractSkillNameFromEvent(event), + ); + const base = { text, toolName, streamKind }; + if ( + streamKind === "assistant_text_delta" || + streamKind === "assistant_text_batch" || + runtimeType === "text_delta" || + runtimeType === "text_delta_batch" + ) { + return { ...base, kind: "assistant_text" }; + } + if ( + streamKind === "thinking_delta" || + runtimeType === "thinking_delta" || + runtimeType === "reasoning_delta" + ) { + return { ...base, kind: "thinking" }; + } + if (streamKind === "tool_input_delta" || runtimeType === "tool_input_delta") { + return { ...base, kind: "tool_input" }; + } + if (streamKind === "tool_output_delta" || runtimeType === "tool_output_delta") { + return { ...base, kind: "tool_output" }; + } + if (event.eventType === ["task", `partial${"Art"}ifact`].join(":")) { + return { ...base, kind: "assistant_text" }; + } + return null; +} + +function isTerminalProcess({ + task, + snapshot, + events, + contract, +}: { + task: unknown; + snapshot: unknown; + events: unknown[]; + contract: unknown; +}): boolean { + if (isRecord(contract) && contract.ok === true) { + return true; + } + const taskRecord = isRecord(task) ? task : {}; + const snapshotRecord = isRecord(snapshot) ? snapshot : {}; + const status = String( + snapshotRecord.taskStatus ?? snapshotRecord.status ?? taskRecord.status ?? "", + ).toLowerCase(); + if ( + [ + "succeeded", + "success", + "completed", + "complete", + "failed", + "failure", + "error", + "cancelled", + "canceled", + ].includes(status) + ) { + return true; + } + return events.some((event) => { + if (!isRecord(event)) { + return false; + } + const type = String( + event.eventType ?? event.type ?? readRuntimeEventType(event) ?? "", + ).toLowerCase(); + const message = String(event.message ?? "").toLowerCase(); + return /task:completed|task:error|task:cancelled|final_done|turn_completed|turn_failed|cancelled|已被中断|已完成/.test( + `${type} ${message}`, + ); + }); +} + +function extractModelFromProcess( + events: unknown[], + task: unknown, + snapshot: unknown, +): AgentAppRuntimeProcessModel { + const values = [task, snapshot, recordValue(snapshot, "threadRead"), ...events, ...events.map(readRuntimeEvent)]; + for (const value of values) { + const decision = findFirstObjectByKeys(value, [ + "routing_decision", + "routingDecision", + "model_routing", + "modelRouting", + "provider_routing", + "providerRouting", + ]); + const target = decision ?? (isRecord(value) ? value : null); + if (!target) { + continue; + } + const provider = stringValue( + findFirstValueByKeys(target, [ + "selected_provider", + "selectedProvider", + "provider", + "providerName", + ]), + ); + const model = stringValue( + findFirstValueByKeys(target, [ + "selected_model", + "selectedModel", + "model", + "modelName", + "model_name", + ]), + ); + if (provider || model) { + return { + provider, + model, + label: [provider, model].filter(Boolean).join("/") || "自动选择", + }; + } + } + return { provider: "", model: "", label: "模型等待路由" }; +} + +function extractUsageFromProcess( + events: unknown[], + snapshot: unknown, + task: unknown, +): AgentAppRuntimeProcessUsage | null { + for (const value of [ + task, + snapshot, + recordValue(snapshot, "threadRead"), + ...events.map(readRuntimeEvent), + ...events, + ]) { + const usage = extractUsageObject(value); + if (usage) { + return usage; + } + } + return estimateUsageFromProcess(events, snapshot, task); +} + +function extractCostFromProcess( + events: unknown[], + snapshot: unknown, + task: unknown, +): AgentAppRuntimeProcessCost | null { + for (const value of [ + task, + snapshot, + recordValue(snapshot, "threadRead"), + ...events.map(readRuntimeEvent), + ...events, + ]) { + const cost = extractCostState(value); + if (cost) { + return cost; + } + } + return null; +} + +function extractUsageObject(value: unknown): AgentAppRuntimeProcessUsage | null { + const usage = findFirstObjectByKeys(value, ["usage", "tokenUsage", "token_usage"]); + if (!usage) { + return null; + } + const input = numberValue(usage.inputTokens ?? usage.input_tokens); + const output = numberValue(usage.outputTokens ?? usage.output_tokens); + const total = numberValue(usage.totalTokens ?? usage.total_tokens) || input + output; + const cachedInputTokens = optionalNumberValue( + usage.cachedInputTokens ?? usage.cached_input_tokens, + ); + const cacheCreationInputTokens = optionalNumberValue( + usage.cacheCreationInputTokens ?? usage.cache_creation_input_tokens, + ); + if (!input && !output && !total && cachedInputTokens === undefined) { + return null; + } + return { + ...usage, + inputTokens: input, + outputTokens: output, + totalTokens: total, + cachedInputTokens, + cacheCreationInputTokens, + }; +} + +function estimateUsageFromProcess( + events: unknown[], + snapshot: unknown, + task: unknown, +): AgentAppRuntimeProcessUsage | null { + const hasRuntimeSignal = events.some((event) => { + if (!isRecord(event)) { + return false; + } + const surface = [ + event.eventType, + event.type, + event.message, + event.toolName, + readRuntimeEventType(event), + ] + .filter(Boolean) + .join(" "); + return /model|routing|tool|skill|artifact|completed|final_done|turn/i.test(surface); + }); + if (!hasRuntimeSignal && !isTerminalProcess({ task, snapshot, events, contract: null })) { + return null; + } + + const inputTokens = estimateTokenCountFromValue([ + recordValue(task, "input"), + recordValue(task, "expectedOutput"), + recordValue(snapshot, "threadRead")?.turns, + ]); + const outputTokens = estimateTokenCountFromValue([ + ...events.map((event) => + isRecord(event) + ? { + message: event.message, + payload: event.payload, + runtimeEvent: readRuntimeEvent(event), + } + : event, + ), + ]); + const totalTokens = inputTokens + outputTokens; + if (totalTokens <= 0) { + return null; + } + return { + inputTokens, + outputTokens, + totalTokens, + estimated: true, + source: "agent_app_runtime_process_estimate", + }; +} + +function estimateTokenCountFromValue(value: unknown): number { + const text = + typeof value === "string" ? value : (JSON.stringify(value ?? "") ?? ""); + const normalized = text.replace(/\s+/g, " ").trim(); + if (!normalized) { + return 0; + } + return Math.max(1, Math.ceil(normalized.length / 4)); +} + +function extractCostState(value: unknown): AgentAppRuntimeProcessCost | null { + const cost = findFirstObjectByKeys(value, ["cost_state", "costState", "cost"]); + if (!cost) { + return null; + } + const estimatedTotalCost = optionalNumberValue( + cost.estimatedTotalCost ?? cost.estimated_total_cost ?? cost.totalCost ?? cost.total_cost, + ); + const estimatedCostClass = stringValue( + cost.estimatedCostClass ?? cost.estimated_cost_class, + ); + const currency = stringValue(cost.currency); + const totalTokens = optionalNumberValue(cost.totalTokens ?? cost.total_tokens); + if ( + estimatedTotalCost === undefined && + !estimatedCostClass && + totalTokens === undefined + ) { + return null; + } + return { + ...cost, + estimatedTotalCost, + estimatedCostClass, + currency, + }; +} + +function collectRequiredSkillNames(value: unknown): string[] { + if (!isRecord(value)) { + return []; + } + return uniqueTextValues([ + ...collectSkillNamesFromList(value.requiredSkills), + ...collectSkillNamesFromList(value.skills), + ...collectSkillNamesFromList(value.skillRefs), + ...collectSkillNamesFromList(recordValue(value, "skillContract")?.requiredSkills), + ...collectSkillNamesFromList(recordValue(recordValue(value, "input"), "agentTaskContract")?.requiredSkills), + ...collectSkillNamesFromList(recordValue(value, "expectedOutput")?.requiredSkills), + ...collectSkillNamesFromList(recordValue(recordValue(value, "expectedOutput"), "skillContract")?.requiredSkills), + ...collectSkillNamesFromList(recordValue(recordValue(value, "metadata"), "contentFactory")?.skillRefs), + ...collectSkillNamesFromList(recordValue(recordValue(recordValue(value, "metadata"), "contentFactory"), "skillContract")?.requiredSkills), + ]); +} + +function collectSkillNamesFromList(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + return value + .map((item) => { + if (typeof item === "string") { + return item; + } + if (!isRecord(item)) { + return ""; + } + return String( + item.skill ?? item.skillName ?? item.skill_name ?? item.id ?? item.name ?? "", + ); + }) + .filter(Boolean); +} + +function extractToolNameFromEvent(event: unknown): string { + if (!isRecord(event)) { + return ""; + } + const payload = recordValue(event, "payload"); + const runtimeEvent = readRuntimeEvent(event); + return ( + readString(event, "toolName") || + readString(event, "tool_name") || + readString(payload, "toolName") || + readString(payload, "tool_name") || + readString(runtimeEvent, "toolName") || + readString(runtimeEvent, "tool_name") || + stringValue(findFirstValueByKeys(runtimeEvent, ["toolName", "tool_name", "name"])) || + "" + ); +} + +function extractSkillNameFromEvent(event: unknown): string { + if (!isRecord(event)) { + return ""; + } + const payload = recordValue(event, "payload"); + const runtimeEvent = readRuntimeEvent(event); + const toolName = extractToolNameFromEvent(event); + return normalizeSkillNameCandidate( + extractSkillNameFromToolName(toolName) || + stringValue(findFirstValueByKeys(event, ["skillName", "skill_name", "command_name"])) || + extractSkillNameFromArguments(readUnknown(runtimeEvent, "arguments")) || + extractSkillNameFromArguments(readUnknown(runtimeEvent, "accumulated_arguments")) || + extractSkillNameFromArguments(readUnknown(runtimeEvent, "accumulatedArguments")) || + extractSkillNameFromArguments(readUnknown(payload, "arguments")) || + extractSkillNameFromArguments(readUnknown(payload, "args")) || + extractSkillNameFromArguments(readUnknown(event, "arguments")) || + extractSkillNameFromText(readString(runtimeEvent, "arguments")) || + extractSkillNameFromText(readString(runtimeEvent, "accumulated_arguments")) || + extractSkillNameFromText(readString(runtimeEvent, "accumulatedArguments")) || + extractSkillNameFromText(readString(payload, "delta")) || + extractSkillNameFromText(readString(event, "message")), + ); +} + +function extractSkillNameFromToolName(toolName: string): string { + const match = String(toolName || "").match(/^Skill\(([^)]+)\)$/i); + return match?.[1] ?? ""; +} + +function extractSkillNameFromText(text: string): string { + const value = String(text || "").trim(); + if (!value) { + return ""; + } + const parsed = parseJsonObject(value); + if (parsed) { + return stringValue( + findFirstValueByKeys(parsed, ["skill", "skillName", "skill_name", "command_name"]), + ); + } + const match = value.match( + /["']?(?:skill|skillName|skill_name|command_name)["']?\s*[:=]\s*["']?([@A-Za-z0-9_:/.-]+)/, + ); + return match?.[1] ?? ""; +} + +function extractSkillNameFromArguments(value: unknown): string { + if (typeof value === "string") { + return extractSkillNameFromText(value); + } + if (Array.isArray(value)) { + for (const item of value) { + const skillName = extractSkillNameFromArguments(item); + if (skillName) { + return skillName; + } + } + return ""; + } + if (!isRecord(value)) { + return ""; + } + return stringValue( + findFirstValueByKeys(value, ["skill", "skillName", "skill_name", "command_name"], 3), + ); +} + +function normalizeSkillNameCandidate(value: string): string { + const text = String(value || "").trim(); + if (!text || /^Skill$/i.test(text)) { + return ""; + } + return /^[\w@:/.-]+$/.test(text) ? text : ""; +} + +function formatToolTitle(toolName: string, skillName: string): string { + const name = formatToolName(toolName, skillName); + if (skillName || /^Skill/i.test(String(toolName || ""))) { + return `Skill · ${skillName || "待解析名称"}`; + } + if (/ToolSearch/i.test(name)) { + return `检索工具 · ${name}`; + } + if (/Bash|Shell|Command/i.test(name)) { + return `本地执行 · ${name}`; + } + if (/^Agent|SubAgent|Team/i.test(name)) { + return `子任务 · ${name}`; + } + return `工具 · ${name || "未命名"}`; +} + +function formatToolName(toolName: string, skillName: string): string { + if (skillName && /^Skill/i.test(String(toolName || ""))) { + return skillName; + } + return String(toolName || "").trim(); +} + +function normalizeToolMessage(message: string, statusText: string): string { + const text = String(message || "").trim(); + if (!text || /^工具\s+\S+\s+(running|completed|failed|started)$/i.test(text)) { + return `状态:${statusText}`; + } + return text; +} + +function normalizeConfirmationMessage(message: string): string { + return /missingContext|blocked|request|review|action/i.test(message) + ? "AI 同事需要补充确认,已在当前工单等待处理。" + : message; +} + +function extractEventDetail(event: Record): string { + const runtimeEvent = readRuntimeEvent(event); + const payload = recordValue(event, "payload"); + const result = recordValue(runtimeEvent, "result"); + const detail = + readString(runtimeEvent, "arguments") || + readString(runtimeEvent, "accumulated_arguments") || + readString(runtimeEvent, "accumulatedArguments") || + readString(result, "output") || + readString(result, "error") || + readString(payload, "summary") || + ""; + return detail ? clipText(formatMaybeJsonText(detail), 520) : ""; +} + +function routingLabel(runtimeEvent: Record): string { + const decision = + findFirstObjectByKeys(runtimeEvent, ["routing_decision", "routingDecision"]) ?? + runtimeEvent; + const provider = + readString(decision, "selected_provider") || readString(decision, "selectedProvider"); + const model = + readString(decision, "selected_model") || + readString(decision, "selectedModel") || + readString(runtimeEvent, "model"); + const candidateCount = numberValue( + decision.candidate_count ?? decision.candidateCount, + ); + const selected = [provider, model].filter(Boolean).join("/"); + if (selected && candidateCount) { + return `已从 ${candidateCount} 个候选中选择 ${selected}`; + } + if (selected) { + return `已选择 ${selected}`; + } + if (candidateCount) { + return `已找到 ${candidateCount} 个候选模型`; + } + return ""; +} + +function runtimeAssetLabel(event: Record): string { + const payload = recordValue(event, "payload"); + const artifact = recordValue(payload, "artifact") ?? recordValue(readRuntimeEvent(event), "artifact"); + return ( + readString(event, "artifactRef") || + readString(payload, "artifactRef") || + readString(artifact, "file_path") || + readString(artifact, "filePath") || + readString(artifact, "kind") || + "" + ); +} + +function evidenceLabel(event: Record): string { + const payload = recordValue(event, "payload"); + return ( + readString(event, "evidenceRef") || + readString(payload, "evidenceRef") || + readString(payload, "message") || + "" + ); +} + +function statusTextFor(status: string): string { + const normalized = String(status || "").toLowerCase(); + if (["completed", "complete", "succeeded", "success", "done", "recorded", "verified"].includes(normalized)) { + return "已完成"; + } + if (["running", "streaming", "started", "thinking"].includes(normalized)) { + return "正在执行"; + } + if (["queued", "pending"].includes(normalized)) { + return "等待中"; + } + if (["failed", "failure", "error"].includes(normalized)) { + return "执行失败"; + } + if (["cancelled", "canceled"].includes(normalized)) { + return "已中断"; + } + return "已更新"; +} + +function readRuntimeEvent(event: unknown): Record { + if (!isRecord(event)) { + return {}; + } + if (isRecord(event.runtimeEvent)) { + return event.runtimeEvent; + } + const payload = recordValue(event, "payload"); + if (isRecord(payload?.runtimeEvent)) { + return payload.runtimeEvent; + } + if (isRecord(payload?.profileEvent)) { + return payload.profileEvent; + } + return {}; +} + +function readRuntimeEventType( + event: unknown, + runtimeEvent: Record = readRuntimeEvent(event), +): string { + return String( + readString(runtimeEvent, "type") || + readString(runtimeEvent, "event_type") || + readString(runtimeEvent, "eventType") || + (isRecord(event) ? readString(event, "eventType") || readString(event, "type") : "") || + "", + ).toLowerCase(); +} + +function findFirstObjectByKeys( + value: unknown, + keys: string[], + depth = 6, +): Record | null { + const found = findFirstValueByKeys(value, keys, depth); + return isRecord(found) ? found : null; +} + +function findFirstValueByKeys(value: unknown, keys: string[], depth = 6): unknown { + if (depth < 0) { + return undefined; + } + if (Array.isArray(value)) { + for (const item of value) { + const found = findFirstValueByKeys(item, keys, depth - 1); + if (found !== undefined && found !== null && found !== "") { + return found; + } + } + return undefined; + } + if (!isRecord(value)) { + return undefined; + } + for (const key of keys) { + const candidate = value[key]; + if (candidate !== undefined && candidate !== null && candidate !== "") { + return candidate; + } + } + for (const child of Object.values(value)) { + const found = findFirstValueByKeys(child, keys, depth - 1); + if (found !== undefined && found !== null && found !== "") { + return found; + } + } + return undefined; +} + +function parseJsonObject(text: string): Record | null { + try { + const parsed = JSON.parse(text); + return isRecord(parsed) ? parsed : null; + } catch { + return null; + } +} + +function formatMaybeJsonText(text: string): string { + const parsed = parseJsonObject(String(text || "").trim()); + return parsed ? JSON.stringify(parsed, null, 2) : String(text || ""); +} + +function clipText(value: string, maxChars: number): string { + const text = String(value || ""); + return text.length > maxChars ? `${text.slice(-maxChars)}\n…已截取最近输出` : text; +} + +function formatNumber(value: number): string { + return new Intl.NumberFormat("zh-CN").format(value); +} + +function uniqueTextValues(values: Array): string[] { + return Array.from( + new Set(values.map((value) => String(value || "").trim()).filter(Boolean)), + ); +} + +function optionalNumberValue(value: unknown): number | undefined { + const number = Number(value); + return Number.isFinite(number) ? number : undefined; +} + +function numberValue(value: unknown): number { + return optionalNumberValue(value) ?? 0; +} + +function stringValue(value: unknown): string { + return typeof value === "string" && value.trim() ? value.trim() : ""; +} + +function readString(value: unknown, key: string): string { + if (!isRecord(value)) { + return ""; + } + const item = value[key]; + return typeof item === "string" && item.trim() ? item.trim() : ""; +} + +function readUnknown(value: unknown, key: string): unknown { + if (!isRecord(value)) { + return undefined; + } + return value[key]; +} + +function recordValue(value: unknown, key: string): Record | undefined { + if (!isRecord(value)) { + return undefined; + } + return isRecord(value[key]) ? value[key] : undefined; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} diff --git a/src/features/agent-app/runtime/capabilityDispatcher.test.ts b/src/features/agent-app/runtime/capabilityDispatcher.test.ts index 567d59550..0edeabc46 100644 --- a/src/features/agent-app/runtime/capabilityDispatcher.test.ts +++ b/src/features/agent-app/runtime/capabilityDispatcher.test.ts @@ -2,9 +2,11 @@ import { describe, expect, it } from "vitest"; import { AdapterCapabilityHost } from "../adapters/AdapterCapabilityHost"; import { InMemoryAgentAppCapabilityStore } from "../adapters/InMemoryAgentAppCapabilityStore"; import { buildInstalledAppPreview } from "../install/installedAppPreview"; +import type { CapabilityHost } from "../sdk/CapabilityHost"; import type { AgentAppArtifactRecord, AgentAppEvidenceRecord, + AgentAppRuntimeProcessView, AgentAppStorageEntry, AgentAppTaskRecord, AgentAppTaskStreamEvent, @@ -40,13 +42,23 @@ function buildCapabilityRequest( }; } -function buildDispatcher() { +function buildDispatcher( + standardProfile: { + manifestVersion?: string; + agentRuntime?: unknown; + requirements?: unknown; + boundary?: unknown; + integrations?: unknown; + operations?: unknown; + } = {}, +) { + const profile = buildWorkflowRuntimeCapabilityProfile({ + realAdapterEnabled: true, + uiRuntimeEnabled: true, + workerRuntimeEnabled: true, + }); const preview = buildInstalledAppPreview({ - profile: buildWorkflowRuntimeCapabilityProfile({ - realAdapterEnabled: true, - uiRuntimeEnabled: true, - workerRuntimeEnabled: true, - }), + profile, loadedAt: FIXED_NOW, checkedAt: FIXED_NOW, generatedAt: FIXED_NOW, @@ -62,6 +74,8 @@ function buildDispatcher() { projection: preview.projection, entryKey: "dashboard", runId: "bridge-run-1", + profile, + ...standardProfile, }); return (request: CapabilityRequestFixture) => dispatch(buildCapabilityRequest(request)); @@ -168,7 +182,1343 @@ function buildDispatcherWithoutCreativeCapabilityAllowlist() { dispatch(buildCapabilityRequest(request)); } +function buildRuntimeProjectionDispatcher() { + const profile = buildWorkflowRuntimeCapabilityProfile({ + realAdapterEnabled: true, + uiRuntimeEnabled: true, + workerRuntimeEnabled: true, + }); + const preview = buildInstalledAppPreview({ + profile, + loadedAt: FIXED_NOW, + checkedAt: FIXED_NOW, + generatedAt: FIXED_NOW, + }); + const runtimeProcess: AgentAppRuntimeProcessView = { + timeline: [ + { + kind: "routing", + title: "模型路由", + statusText: "已决策", + message: "openai/gpt-4.1", + }, + { + kind: "metrics", + title: "消耗统计", + statusText: "已记录", + message: "1540 tokens", + }, + { + kind: "tool", + title: "Tool · web_search", + statusText: "已完成", + message: "检索竞品资料来源。", + meta: "web_search:run-1", + }, + { + kind: "tool", + title: "Tool · browser.extract", + statusText: "已完成", + message: "读取网页正文。", + meta: "browser:run-1", + }, + { + kind: "tool", + title: "Tool · document_parser", + statusText: "已完成", + message: "解析 PDF brief。", + meta: "document:run-1", + }, + { + kind: "tool", + title: "Tool · image_generation", + statusText: "已完成", + message: "生成配套封面图。", + meta: "media:run-1", + }, + { + kind: "tool", + title: "Tool · mcp__research__search", + statusText: "已完成", + message: "读取 MCP research server 结果。", + meta: "mcp:run-1", + }, + { + kind: "execution", + title: "Tool · terminal.run", + statusText: "已完成", + message: "执行受控命令生成素材清单。", + meta: "terminal:run-1", + }, + { + kind: "tool", + title: "Tool · connector__notion__createPage", + statusText: "已完成", + message: "通过外部连接器写入内容看板。", + meta: "connector:run-1", + }, + ], + streamText: "已生成内容策略。", + thinkingText: "", + executionText: "", + skillNames: ["content-strategist"], + invokedSkillNames: ["content-strategist"], + model: { provider: "openai", model: "gpt-4.1", label: "openai/gpt-4.1" }, + usage: { + inputTokens: 1200, + outputTokens: 340, + totalTokens: 1540, + cachedInputTokens: 100, + cacheCreationInputTokens: 40, + }, + cost: { + estimatedTotalCost: 0.043, + estimatedCostClass: "low", + currency: "USD", + }, + terminal: true, + collapsedByDefault: true, + routingCount: 1, + executionCount: 1, + artifactCount: 1, + }; + const task: AgentAppTaskRecord = { + taskId: "agent-app-task-1", + traceId: "agent-app-trace-1", + appId: "content-factory-app", + entryKey: "dashboard", + title: "生成内容场景", + prompt: "基于项目知识生成内容场景", + taskKind: "content.scenario_planning", + idempotencyKey: "dashboard:scenario", + input: { projectId: "project-1" }, + expectedOutput: { artifactKind: "content_table" }, + knowledge: [ + { + key: "project_knowledge", + mode: "retrieval", + required: true, + }, + ], + tools: ["content-strategist"], + files: [], + secrets: [], + humanReview: true, + status: "succeeded", + startedAt: FIXED_NOW, + finishedAt: "2026-05-15T00:01:00.000Z", + result: { + thread_id: "agent-runtime-thread-1", + status: "completed", + diagnostics: { + context_compaction_count: 1, + pending_request_count: 0, + }, + turns: [ + { + turn_id: "agent-runtime-turn-1", + status: "completed", + }, + ], + telemetry_summary: { + join_status: "available", + trace_ids: ["agent-app-trace-1"], + }, + summary: "已生成内容策略。", + }, + trace: [], + events: [], + runtimeProcess, + process: runtimeProcess, + provenance: { + sourceKind: "agent_app", + appId: "content-factory-app", + appVersion: preview.identity.appVersion, + packageHash: preview.identity.packageHash, + manifestHash: preview.identity.manifestHash, + entryKey: "dashboard", + taskId: "agent-app-task-1", + }, + }; + const host: CapabilityHost = { + createSdkContext: () => { + throw new Error("runtime projection test should not create SDK context"); + }, + runEntry: async () => { + throw new Error("runtime projection test should not run entries"); + }, + getArtifacts: () => [], + getEvidence: () => [], + getStorageEntries: () => [], + getTasks: () => [task], + uninstall: async () => ({ + appId: "content-factory-app", + mode: "keep-data", + deletedTargets: [], + retainedTargets: [], + warnings: [], + }), + }; + const projection = { + ...preview.projection, + requiredCapabilities: [ + ...preview.projection.requiredCapabilities, + { + capability: "lime.models", + requestedRange: "^0.3.0", + required: true, + declaredBy: ["requires" as const], + }, + { + capability: "lime.usage", + requestedRange: "^0.3.0", + required: true, + declaredBy: ["requires" as const], + }, + { + capability: "lime.skills", + requestedRange: "^0.3.0", + required: true, + declaredBy: ["requires" as const], + }, + { + capability: "lime.memory", + requestedRange: "^0.3.0", + required: true, + declaredBy: ["requires" as const], + }, + { + capability: "lime.context", + requestedRange: "^0.3.0", + required: true, + declaredBy: ["requires" as const], + }, + { + capability: "lime.search", + requestedRange: "^0.3.0", + required: true, + declaredBy: ["requires" as const], + }, + { + capability: "lime.browser", + requestedRange: "^0.3.0", + required: true, + declaredBy: ["requires" as const], + }, + { + capability: "lime.documents", + requestedRange: "^0.3.0", + required: true, + declaredBy: ["requires" as const], + }, + { + capability: "lime.media", + requestedRange: "^0.3.0", + required: true, + declaredBy: ["requires" as const], + }, + { + capability: "lime.mcp", + requestedRange: "^0.3.0", + required: true, + declaredBy: ["requires" as const], + }, + { + capability: "lime.terminal", + requestedRange: "^0.3.0", + required: true, + declaredBy: ["requires" as const], + }, + { + capability: "lime.connectors", + requestedRange: "^0.3.0", + required: true, + declaredBy: ["requires" as const], + }, + ], + }; + const dispatch = createAgentAppCapabilityDispatcher({ + host, + projection, + entryKey: "dashboard", + profile, + }); + return (request: CapabilityRequestFixture) => + dispatch(buildCapabilityRequest(request)); +} + describe("createAgentAppCapabilityDispatcher", () => { + it("应通过 lime.capabilities 暴露 Host discovery profile,且不泄露内部路径", async () => { + const dispatch = buildDispatcher(); + + const list = (await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.capabilities", + method: "list", + rawPayload: { + capability: "lime.capabilities", + method: "list", + }, + })) as Array>; + + const agent = list.find((item) => item.name === "lime.agent"); + const discovery = list.find((item) => item.name === "lime.capabilities"); + expect(agent).toMatchObject({ + name: "lime.agent", + version: "0.3.0", + stage: "current", + owner: "agent_runtime", + enabled: true, + implementation: "adapter", + }); + expect(discovery).toMatchObject({ + name: "lime.capabilities", + stage: "preview", + owner: "desktop_host", + enabled: true, + implementation: "native", + }); + expect(discovery).not.toHaveProperty("unavailableReason"); + expect(Object.keys(discovery ?? {})).not.toEqual( + expect.arrayContaining(["path", "sourceFile", "internal"]), + ); + + const single = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.capabilities", + method: "get", + input: { capability: "lime.agent" }, + rawPayload: { + capability: "lime.capabilities", + method: "get", + }, + }); + expect(single).toMatchObject({ + name: "lime.agent", + methods: expect.arrayContaining(["startTask", "streamTask", "getTask"]), + enabled: true, + }); + + const profile = (await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.capabilities", + method: "getProfile", + rawPayload: { + capability: "lime.capabilities", + method: "getProfile", + }, + })) as Record; + expect(profile).toMatchObject({ + appRuntimeVersion: "0.7.0", + standardVersions: { + current: "0.7", + compatible: ["0.5", "0.6", "0.7"], + }, + standards: expect.objectContaining({ + layeredManifest: expect.objectContaining({ + version: "0.5", + enabled: true, + layerFiles: expect.arrayContaining(["app.capabilities.yaml"]), + }), + agentRuntime: expect.objectContaining({ + version: "0.6", + enabled: false, + layerFiles: ["app.runtime.yaml"], + }), + requirementBoundary: expect.objectContaining({ + version: "0.7", + enabled: false, + layerFiles: expect.arrayContaining([ + "app.requirements.yaml", + "app.boundary.yaml", + "app.integrations.yaml", + "app.operations.yaml", + ]), + hostCloudManagedExecution: true, + externalSideEffectsRequireApproval: true, + }), + }), + runtimeTargets: ["local"], + capabilities: expect.objectContaining({ + "lime.capabilities": expect.objectContaining({ + enabled: true, + implementation: "native", + }), + "lime.models": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.usage": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.skills": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.memory": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.context": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.search": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.browser": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.documents": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.media": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.mcp": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.terminal": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.connectors": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + }), + }); + + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.capabilities", + method: "get", + input: { capability: "lime.unknown" }, + rawPayload: { + capability: "lime.capabilities", + method: "get", + }, + }), + ).rejects.toMatchObject({ + code: "CAPABILITY_NOT_FOUND", + }); + }); + + it("应通过 lime.capabilities.getProfile 暴露 v0.7 需求边界与能力交接合同", async () => { + const requirements = { + requirements: [ + { + id: "CF-R001", + text: "生成可审核内容草稿", + priority: "mvp", + }, + ], + nonGoals: ["不在 App 包内保存外部凭证"], + }; + const boundary = { + boundaries: [ + { + requirementId: "CF-R001", + planes: { + app: { owns: ["workflow_state"] }, + host: { requires: ["lime.agent", "lime.evidence"] }, + }, + }, + ], + }; + const integrations = [ + { + key: "planning_table", + provider: "cloud.table", + executionPlane: "hybrid", + hostCapability: "lime.connectors", + }, + ]; + const operations = [ + { + key: "write_external_draft", + type: "external_write", + sideEffect: "external_write", + approvalRequired: true, + dryRunRequired: true, + evidenceRequired: true, + autoExecute: false, + }, + ]; + const dispatch = buildDispatcher({ + manifestVersion: "0.7", + requirements, + boundary, + integrations, + operations, + }); + + const profile = (await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.capabilities", + method: "getProfile", + rawPayload: { + capability: "lime.capabilities", + method: "getProfile", + }, + })) as Record; + + expect(profile).toMatchObject({ + appRuntimeVersion: "0.7.0", + standardVersions: { + current: "0.7", + compatible: ["0.5", "0.6", "0.7"], + }, + requirements, + boundary, + integrations, + operations, + standards: expect.objectContaining({ + requirementBoundary: expect.objectContaining({ + version: "0.7", + enabled: true, + manifestVersion: "0.7", + requirementCount: 1, + boundaryCount: 1, + integrationCount: 1, + operationCount: 1, + appCredentialsBoundary: "host_or_cloud_managed", + }), + }), + }); + }); + + it("应通过 lime.models / lime.usage 投影 AgentRuntime 模型与用量事实", async () => { + const dispatch = buildRuntimeProjectionDispatcher(); + + const models = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.models", + method: "list", + rawPayload: { + capability: "lime.models", + method: "list", + }, + }); + expect(models).toMatchObject({ + appId: "content-factory-app", + source: "agent_runtime_projection", + taskCount: 1, + models: [ + expect.objectContaining({ + provider: "openai", + model: "gpt-4.1", + label: "openai/gpt-4.1", + taskCount: 1, + taskKinds: ["content.scenario_planning"], + }), + ], + }); + + const routing = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.models", + method: "getRouting", + input: { taskId: "agent-app-task-1" }, + rawPayload: { + capability: "lime.models", + method: "getRouting", + }, + }); + expect(routing).toMatchObject({ + source: "agent_runtime_projection", + routes: [ + expect.objectContaining({ + taskId: "agent-app-task-1", + model: { + provider: "openai", + model: "gpt-4.1", + label: "openai/gpt-4.1", + }, + }), + ], + }); + + const tokenUsage = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.usage", + method: "getTokenUsage", + input: { taskId: "agent-app-task-1" }, + rawPayload: { + capability: "lime.usage", + method: "getTokenUsage", + }, + }); + expect(tokenUsage).toMatchObject({ + appId: "content-factory-app", + source: "agent_runtime_projection", + taskCount: 1, + totals: { + inputTokens: 1200, + outputTokens: 340, + totalTokens: 1540, + cachedInputTokens: 100, + cacheCreationInputTokens: 40, + }, + tasks: [ + expect.objectContaining({ + taskId: "agent-app-task-1", + usage: expect.objectContaining({ totalTokens: 1540 }), + }), + ], + }); + + const costSummary = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.usage", + method: "getCostSummary", + input: { taskId: "agent-app-task-1" }, + rawPayload: { + capability: "lime.usage", + method: "getCostSummary", + }, + }); + expect(costSummary).toMatchObject({ + source: "agent_runtime_projection", + cost: { + estimatedTotalCost: 0.043, + currency: "USD", + }, + }); + }); + + it("应通过 lime.skills 投影 AgentRuntime Skill 声明与调用事实", async () => { + const dispatch = buildRuntimeProjectionDispatcher(); + + const listed = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.skills", + method: "list", + rawPayload: { + capability: "lime.skills", + method: "list", + }, + }); + expect(listed).toMatchObject({ + appId: "content-factory-app", + source: "agent_runtime_process", + taskCount: 1, + skills: [ + expect.objectContaining({ + skillId: "content-strategist", + name: "content-strategist", + status: "invoked", + taskCount: 1, + invocationCount: 1, + taskIds: ["agent-app-task-1"], + taskKinds: ["content.scenario_planning"], + source: "agent_runtime_process", + }), + ], + }); + + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.skills", + method: "resolve", + input: { skillId: "content-strategist" }, + rawPayload: { + capability: "lime.skills", + method: "resolve", + }, + }), + ).resolves.toMatchObject({ + skillId: "content-strategist", + status: "invoked", + invocationCount: 1, + }); + + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.skills", + method: "getInvocation", + input: { invocationId: "agent-app-task-1:content-strategist" }, + rawPayload: { + capability: "lime.skills", + method: "getInvocation", + }, + }), + ).resolves.toMatchObject({ + invocationId: "agent-app-task-1:content-strategist", + skillId: "content-strategist", + taskId: "agent-app-task-1", + status: "succeeded", + source: "agent_runtime_process", + }); + + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.skills", + method: "bind", + input: { skillId: "content-strategist" }, + rawPayload: { + capability: "lime.skills", + method: "bind", + }, + }), + ).resolves.toEqual({ + status: "not_available", + reason: "skill_runtime_mutation_not_exposed_to_agent_apps", + source: "agent_runtime_process", + }); + }); + + it("应通过 lime.memory / lime.context 投影只读记忆与上下文状态", async () => { + const dispatch = buildRuntimeProjectionDispatcher(); + + const memoryStatus = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.memory", + method: "getStatus", + rawPayload: { + capability: "lime.memory", + method: "getStatus", + }, + }); + expect(memoryStatus).toMatchObject({ + appId: "content-factory-app", + status: "read_only_projection", + source: "agent_runtime_projection", + writable: false, + compactable: false, + totals: { + knowledgeBindingCount: 1, + contextCompactionCount: 1, + pendingRequestCount: 0, + }, + observations: [ + expect.objectContaining({ + taskId: "agent-app-task-1", + knowledgeBindingKeys: ["project_knowledge"], + contextCompactionCount: 1, + }), + ], + }); + + const memoryQuery = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.memory", + method: "query", + input: { query: "project_knowledge" }, + rawPayload: { + capability: "lime.memory", + method: "query", + }, + }); + expect(memoryQuery).toMatchObject({ + status: "limited_projection", + source: "agent_runtime_projection", + records: [ + expect.objectContaining({ + taskId: "agent-app-task-1", + knowledgeBindingKeys: ["project_knowledge"], + }), + ], + }); + + const contextSnapshot = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.context", + method: "getSnapshot", + rawPayload: { + capability: "lime.context", + method: "getSnapshot", + }, + }); + expect(contextSnapshot).toMatchObject({ + appId: "content-factory-app", + source: "agent_runtime_projection", + taskCount: 1, + contexts: [ + expect.objectContaining({ + taskId: "agent-app-task-1", + traceId: "agent-app-trace-1", + threadId: "agent-runtime-thread-1", + turnIds: ["agent-runtime-turn-1"], + knowledgeBindingKeys: ["project_knowledge"], + toolKeys: ["content-strategist"], + inputAttached: true, + expectedOutputAttached: true, + }), + ], + }); + + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.memory", + method: "write", + input: { scope: "task", value: { note: "不要直接写入" } }, + rawPayload: { + capability: "lime.memory", + method: "write", + }, + }), + ).resolves.toEqual({ + status: "not_available", + reason: "memory_runtime_mutation_not_exposed_to_agent_apps", + source: "agent_runtime_projection", + }); + + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.memory", + method: "compact", + input: { scope: "task" }, + rawPayload: { + capability: "lime.memory", + method: "compact", + }, + }), + ).resolves.toEqual({ + status: "not_available", + reason: "memory_runtime_mutation_not_exposed_to_agent_apps", + source: "agent_runtime_projection", + }); + + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.context", + method: "attach", + input: { ref: "artifact-1" }, + rawPayload: { + capability: "lime.context", + method: "attach", + }, + }), + ).resolves.toEqual({ + status: "not_available", + reason: "context_mutation_not_exposed_to_agent_apps", + source: "agent_runtime_projection", + }); + + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.context", + method: "detach", + input: { ref: "artifact-1" }, + rawPayload: { + capability: "lime.context", + method: "detach", + }, + }), + ).resolves.toEqual({ + status: "not_available", + reason: "context_mutation_not_exposed_to_agent_apps", + source: "agent_runtime_projection", + }); + }); + + it("应通过 ToolRuntime preview capabilities 暴露受控工具意图和运行投影", async () => { + const dispatch = buildRuntimeProjectionDispatcher(); + + const searchIntent = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.search", + method: "query", + input: { query: "竞品资料", limit: 3 }, + rawPayload: { + capability: "lime.search", + method: "query", + }, + }); + expect(searchIntent).toMatchObject({ + appId: "content-factory-app", + capability: "lime.search", + method: "query", + status: "requires_agent_task", + reason: "search_execution_requires_lime_agent_task", + source: "tool_runtime_policy", + intent: { query: "竞品资料", limit: 3 }, + toolHints: ["lime.capability.research.search", "web_search"], + next: { + capability: "lime.agent", + method: "startTask", + }, + matchingRuns: [ + expect.objectContaining({ + runId: "web_search:run-1", + capability: "lime.search", + toolName: "web_search", + taskId: "agent-app-task-1", + source: "agent_runtime_process", + }), + ], + }); + + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.search", + method: "getRun", + input: { runId: "web_search:run-1" }, + rawPayload: { + capability: "lime.search", + method: "getRun", + }, + }), + ).resolves.toMatchObject({ + runId: "web_search:run-1", + capability: "lime.search", + status: "succeeded", + title: "Tool · web_search", + }); + + const browserIntent = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.browser", + method: "open", + input: { url: "https://example.com/brief" }, + rawPayload: { + capability: "lime.browser", + method: "open", + }, + }); + expect(browserIntent).toMatchObject({ + capability: "lime.browser", + method: "open", + status: "requires_agent_task", + reason: "browser_runtime_execution_requires_lime_tool_runtime_policy", + source: "tool_runtime_policy", + intent: { url: "https://example.com/brief" }, + matchingRuns: [ + expect.objectContaining({ + runId: "browser:run-1", + capability: "lime.browser", + toolName: "browser.extract", + }), + ], + }); + + const documentIntent = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.documents", + method: "parse", + input: { ref: "file:brief.pdf" }, + rawPayload: { + capability: "lime.documents", + method: "parse", + }, + }); + expect(documentIntent).toMatchObject({ + capability: "lime.documents", + method: "parse", + status: "requires_agent_task", + reason: "document_runtime_execution_requires_lime_tool_runtime_policy", + source: "tool_runtime_policy", + intent: { ref: "file:brief.pdf" }, + matchingRuns: [ + expect.objectContaining({ + runId: "document:run-1", + capability: "lime.documents", + toolName: "document_parser", + }), + ], + }); + + const mediaIntent = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.media", + method: "generateImage", + input: { prompt: "生成一张内容活动封面", size: "1024x1024" }, + rawPayload: { + capability: "lime.media", + method: "generateImage", + }, + }); + expect(mediaIntent).toMatchObject({ + capability: "lime.media", + method: "generateImage", + status: "requires_agent_task", + reason: "media_runtime_execution_requires_lime_tool_runtime_policy", + source: "tool_runtime_policy", + intent: { prompt: "生成一张内容活动封面", size: "1024x1024" }, + matchingRuns: [ + expect.objectContaining({ + runId: "media:run-1", + capability: "lime.media", + toolName: "image_generation", + }), + ], + }); + + const mcpServers = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.mcp", + method: "listServers", + rawPayload: { + capability: "lime.mcp", + method: "listServers", + }, + }); + expect(mcpServers).toMatchObject({ + appId: "content-factory-app", + status: "read_only_projection", + source: "agent_runtime_process", + servers: [ + expect.objectContaining({ + serverId: "research", + toolCount: 1, + runIds: ["mcp:run-1"], + }), + ], + }); + + const mcpIntent = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.mcp", + method: "invoke", + input: { tool: "mcp__research__search", input: { query: "竞品" } }, + rawPayload: { + capability: "lime.mcp", + method: "invoke", + }, + }); + expect(mcpIntent).toMatchObject({ + capability: "lime.mcp", + method: "invoke", + status: "requires_agent_task", + reason: "mcp_execution_requires_lime_tool_runtime_policy", + source: "tool_runtime_policy", + intent: { tool: "mcp__research__search", input: { query: "竞品" } }, + matchingRuns: [ + expect.objectContaining({ + runId: "mcp:run-1", + capability: "lime.mcp", + toolName: "mcp__research__search", + }), + ], + }); + + const terminalIntent = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.terminal", + method: "run", + input: { command: "npm run build:assets", reason: "生成素材清单" }, + rawPayload: { + capability: "lime.terminal", + method: "run", + }, + }); + expect(terminalIntent).toMatchObject({ + capability: "lime.terminal", + method: "run", + status: "requires_agent_task", + reason: "terminal_execution_requires_lime_sandbox_policy", + source: "tool_runtime_policy", + intent: { + command: "npm run build:assets", + reason: "生成素材清单", + }, + matchingRuns: [ + expect.objectContaining({ + runId: "terminal:run-1", + capability: "lime.terminal", + toolName: "terminal.run", + }), + ], + }); + + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.terminal", + method: "getRun", + input: { runId: "terminal:run-1" }, + rawPayload: { + capability: "lime.terminal", + method: "getRun", + }, + }), + ).resolves.toMatchObject({ + runId: "terminal:run-1", + capability: "lime.terminal", + status: "succeeded", + }); + + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.terminal", + method: "cancel", + input: { runId: "terminal:run-1" }, + rawPayload: { + capability: "lime.terminal", + method: "cancel", + }, + }), + ).resolves.toEqual({ + status: "not_available", + reason: "terminal_runtime_cancellation_not_exposed_to_agent_apps", + source: "tool_runtime_policy", + }); + + const connectors = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.connectors", + method: "list", + rawPayload: { + capability: "lime.connectors", + method: "list", + }, + }); + expect(connectors).toMatchObject({ + appId: "content-factory-app", + status: "read_only_projection", + source: "agent_runtime_process", + connectors: [ + expect.objectContaining({ + connectorId: "notion", + actionIds: ["createPage"], + runIds: ["connector:run-1"], + }), + ], + }); + + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.connectors", + method: "getStatus", + input: { connectorId: "notion" }, + rawPayload: { + capability: "lime.connectors", + method: "getStatus", + }, + }), + ).resolves.toMatchObject({ + connectorId: "notion", + status: "observed", + source: "agent_runtime_process", + }); + + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.connectors", + method: "requestAuth", + input: { connectorId: "notion", reason: "同步内容看板" }, + rawPayload: { + capability: "lime.connectors", + method: "requestAuth", + }, + }), + ).resolves.toMatchObject({ + capability: "lime.connectors", + method: "requestAuth", + status: "requires_host_authorization", + reason: "connector_auth_requires_lime_policy_and_secret_binding", + intent: { connectorId: "notion", reason: "同步内容看板" }, + }); + + const connectorIntent = await dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.connectors", + method: "invoke", + input: { + connectorId: "notion", + action: "createPage", + input: { title: "内容计划" }, + }, + rawPayload: { + capability: "lime.connectors", + method: "invoke", + }, + }); + expect(connectorIntent).toMatchObject({ + capability: "lime.connectors", + method: "invoke", + status: "requires_agent_task", + reason: "connector_execution_requires_lime_policy_and_secret_binding", + source: "tool_runtime_policy", + intent: { + connectorId: "notion", + action: "createPage", + input: { title: "内容计划" }, + }, + matchingRuns: [ + expect.objectContaining({ + runId: "connector:run-1", + capability: "lime.connectors", + toolName: "connector__notion__createPage", + }), + ], + }); + }); + + it("应拒绝未声明的 runtime projection capability,避免 App 绕过 manifest", async () => { + const dispatch = buildDispatcher(); + + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.usage", + method: "getTokenUsage", + rawPayload: { + capability: "lime.usage", + method: "getTokenUsage", + }, + }), + ).rejects.toMatchObject({ + code: "CAPABILITY_NOT_DECLARED", + }); + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.skills", + method: "list", + rawPayload: { + capability: "lime.skills", + method: "list", + }, + }), + ).rejects.toMatchObject({ + code: "CAPABILITY_NOT_DECLARED", + }); + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.memory", + method: "getStatus", + rawPayload: { + capability: "lime.memory", + method: "getStatus", + }, + }), + ).rejects.toMatchObject({ + code: "CAPABILITY_NOT_DECLARED", + }); + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.context", + method: "getSnapshot", + rawPayload: { + capability: "lime.context", + method: "getSnapshot", + }, + }), + ).rejects.toMatchObject({ + code: "CAPABILITY_NOT_DECLARED", + }); + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.search", + method: "query", + input: { query: "竞品资料" }, + rawPayload: { + capability: "lime.search", + method: "query", + }, + }), + ).rejects.toMatchObject({ + code: "CAPABILITY_NOT_DECLARED", + }); + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.media", + method: "generateImage", + input: { prompt: "生成配图" }, + rawPayload: { + capability: "lime.media", + method: "generateImage", + }, + }), + ).rejects.toMatchObject({ + code: "CAPABILITY_NOT_DECLARED", + }); + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.terminal", + method: "run", + input: { command: "npm run build" }, + rawPayload: { + capability: "lime.terminal", + method: "run", + }, + }), + ).rejects.toMatchObject({ + code: "CAPABILITY_NOT_DECLARED", + }); + await expect( + dispatch({ + appId: "content-factory-app", + entryKey: "dashboard", + capability: "lime.connectors", + method: "invoke", + input: { connectorId: "notion", action: "createPage" }, + rawPayload: { + capability: "lime.connectors", + method: "invoke", + }, + }), + ).rejects.toMatchObject({ + code: "CAPABILITY_NOT_DECLARED", + }); + }); + it("应把 App capability 请求收敛到 Lime Agent task,并保留 stream/cancel 事件", async () => { const dispatch = buildDispatcher(); diff --git a/src/features/agent-app/runtime/capabilityDispatcher.ts b/src/features/agent-app/runtime/capabilityDispatcher.ts index 3993bd571..25293e2fb 100644 --- a/src/features/agent-app/runtime/capabilityDispatcher.ts +++ b/src/features/agent-app/runtime/capabilityDispatcher.ts @@ -1,5 +1,24 @@ +import { + compatibleAgentAppStandardVersions, + currentAgentAppStandardVersion, + p0HostCapabilityProfile, +} from "../readiness/hostCapabilityProfile"; import type { CapabilityHost, LimeAppSdk } from "../sdk/CapabilityHost"; -import type { AgentAppProjection, AgentAppTaskRequest } from "../types"; +import { + LIME_CAPABILITY_DEFINITIONS, + type LimeCapabilityDefinitionRecord, + type LimeCapabilityName, +} from "../sdk/capabilityCatalog"; +import type { + AgentAppProjection, + AgentAppRuntimeProcessCost, + AgentAppRuntimeProcessModel, + AgentAppRuntimeProcessTimelineItem, + AgentAppRuntimeProcessUsage, + AgentAppTaskRecord, + AgentAppTaskRequest, + HostCapabilityProfile, +} from "../types"; import type { AgentAppHostBridgeCapabilityRequest } from "./hostBridge"; export type AgentAppCapabilityDispatcher = ( @@ -11,6 +30,13 @@ export interface CreateAgentAppCapabilityDispatcherOptions { projection: AgentAppProjection; entryKey: string; runId?: string; + profile?: HostCapabilityProfile; + manifestVersion?: string; + agentRuntime?: unknown; + requirements?: unknown; + boundary?: unknown; + integrations?: unknown; + operations?: unknown; } export class AgentAppCapabilityDispatcherError extends Error { @@ -23,11 +49,13 @@ export class AgentAppCapabilityDispatcherError extends Error { } } -const CREATIVE_CAPABILITY_TOOL_KEYS = new Set([ - "creative_capability_search", - "claw_capability_catalog", - "agent_runtime_capability_catalog", -].map(capabilityMatchToken)); +const CREATIVE_CAPABILITY_TOOL_KEYS = new Set( + [ + "creative_capability_search", + "claw_capability_catalog", + "agent_runtime_capability_catalog", + ].map(capabilityMatchToken), +); const CLAW_CAPABILITY_ALIASES: Array<{ capabilityId: string; @@ -86,6 +114,150 @@ const CLAW_CAPABILITY_ALIASES: Array<{ }, ]; +interface CapabilityDiscoveryEntry { + name: LimeCapabilityName; + version: string; + group: LimeCapabilityDefinitionRecord["group"]; + stage: LimeCapabilityDefinitionRecord["stage"]; + owner: LimeCapabilityDefinitionRecord["owner"]; + methods: string[]; + summary: string; + enabled: boolean; + implementation: HostCapabilityProfile["capabilities"][string]["implementation"]; + unavailableReason?: "disabled" | "not_implemented" | "planned"; +} + +type CapabilityProfileSupport = HostCapabilityProfile["capabilities"][string]; + +interface RuntimeModelProjection { + taskId: string; + taskKind: string; + status: AgentAppTaskRecord["status"]; + startedAt: string; + finishedAt?: string; + model: AgentAppRuntimeProcessModel; +} + +interface RuntimeUsageProjection { + taskId: string; + taskKind: string; + status: AgentAppTaskRecord["status"]; + startedAt: string; + finishedAt?: string; + usage: AgentAppRuntimeProcessUsage; + model: AgentAppRuntimeProcessModel; +} + +interface RuntimeCostProjection { + taskId: string; + taskKind: string; + status: AgentAppTaskRecord["status"]; + startedAt: string; + finishedAt?: string; + cost: AgentAppRuntimeProcessCost; + model: AgentAppRuntimeProcessModel; +} + +interface RuntimeSkillProjection { + skillId: string; + name: string; + status: "declared" | "invoked"; + taskCount: number; + invocationCount: number; + taskIds: string[]; + taskKinds: string[]; + lastSeenAt: string; + source: "agent_runtime_process"; +} + +interface RuntimeSkillInvocationProjection { + invocationId: string; + skillId: string; + name: string; + taskId: string; + taskKind: string; + status: AgentAppTaskRecord["status"]; + startedAt: string; + finishedAt?: string; + source: "agent_runtime_process"; +} + +interface RuntimeMemoryProjection { + taskId: string; + taskKind: string; + status: AgentAppTaskRecord["status"]; + startedAt: string; + finishedAt?: string; + scope: "task"; + knowledgeBindingKeys: string[]; + contextCompactionCount: number; + pendingRequestCount: number; + source: "agent_runtime_projection"; +} + +interface RuntimeContextProjection { + taskId: string; + traceId: string; + taskKind: string; + status: AgentAppTaskRecord["status"]; + startedAt: string; + finishedAt?: string; + workspaceId?: string; + threadId?: string; + turnIds: string[]; + knowledgeBindingKeys: string[]; + toolKeys: string[]; + fileRefs: string[]; + inputAttached: boolean; + expectedOutputAttached: boolean; + pendingRequestCount: number; + source: "agent_runtime_projection"; +} + +type ToolIntegrationCapability = + | "lime.search" + | "lime.browser" + | "lime.documents" + | "lime.media" + | "lime.mcp" + | "lime.terminal" + | "lime.connectors"; + +interface RuntimeToolRunProjection { + runId: string; + capability: ToolIntegrationCapability; + toolName: string; + taskId: string; + taskKind: string; + status: AgentAppTaskRecord["status"] | "declared" | "observed"; + startedAt: string; + finishedAt?: string; + title: string; + statusText: string; + message: string; + detail?: string; + source: "agent_runtime_process"; +} + +interface RuntimeMcpToolProjection { + toolName: string; + serverId: string; + toolId: string; + runIds: string[]; + taskIds: string[]; + lastSeenAt: string; + source: "agent_runtime_process"; +} + +interface RuntimeConnectorProjection { + connectorId: string; + actionIds: string[]; + runIds: string[]; + taskIds: string[]; + lastSeenAt: string; + source: "agent_runtime_process"; +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } @@ -114,6 +286,28 @@ function readInputRecord( ); } +function readOptionalInputRecord( + request: AgentAppHostBridgeCapabilityRequest, +): Record { + if (isRecord(request.input)) { + return request.input; + } + if (request.input !== undefined) { + throw new AgentAppCapabilityDispatcherError( + "INVALID_CAPABILITY_INPUT", + `${request.capability}.${request.method} requires an input object.`, + ); + } + if (isRecord(request.invokeRequest?.args)) { + return request.invokeRequest.args; + } + const firstArg = request.args?.[0]; + if (isRecord(firstArg)) { + return firstArg; + } + return {}; +} + function readStringParam( request: AgentAppHostBridgeCapabilityRequest, key: string, @@ -136,6 +330,21 @@ function readStringParam( return value; } +function readOptionalStringParam( + request: AgentAppHostBridgeCapabilityRequest, + key: string, + argIndex: number, +): string | undefined { + const fromInput = isRecord(request.input) + ? readString(request.input[key]) + : undefined; + const fromInvokeArgs = isRecord(request.invokeRequest?.args) + ? readString(request.invokeRequest.args[key]) + : undefined; + const fromArgs = readString(request.args?.[argIndex]); + return fromInput ?? fromInvokeArgs ?? fromArgs; +} + function hasOwn(value: Record, key: string): boolean { return Object.prototype.hasOwnProperty.call(value, key); } @@ -166,7 +375,9 @@ function resolveClawCapabilityId(value: string): string | null { return descriptor?.capabilityId ?? null; } -function collectRequestedClawCapabilityIds(input: Record): string[] { +function collectRequestedClawCapabilityIds( + input: Record, +): string[] { const requested = [ ...normalizeStringList(input.requiredCapabilities), ...normalizeStringList(input.capabilityHints), @@ -202,7 +413,9 @@ function toolRequirementDeclaresClawCapability( ): boolean { const toolKeyToken = capabilityMatchToken(tool.key); if ( - descriptor.aliases.some((alias) => capabilityMatchToken(alias) === toolKeyToken) + descriptor.aliases.some( + (alias) => capabilityMatchToken(alias) === toolKeyToken, + ) ) { return true; } @@ -210,7 +423,8 @@ function toolRequirementDeclaresClawCapability( return false; } return tool.capabilities.some( - (capability) => resolveClawCapabilityId(capability) === descriptor.capabilityId, + (capability) => + resolveClawCapabilityId(capability) === descriptor.capabilityId, ); } @@ -437,10 +651,9 @@ async function dispatchAgent( request.method === "respondAction" ) { return sdk.agent.submitHostResponse( - readInputRecord( - request, - request.method, - ) as unknown as Parameters[0], + readInputRecord(request, request.method) as unknown as Parameters< + typeof sdk.agent.submitHostResponse + >[0], ); } if (request.method === "listTasks") { @@ -458,15 +671,1554 @@ function throwUnsupportedMethod( ); } +function resolveCapabilityDefinition( + name: string, +): LimeCapabilityDefinitionRecord { + const definition = LIME_CAPABILITY_DEFINITIONS.find( + (item) => item.name === name, + ); + if (!definition) { + throw new AgentAppCapabilityDispatcherError( + "CAPABILITY_NOT_FOUND", + `${name} is not a known Lime capability.`, + ); + } + return definition; +} + +function buildCapabilityDiscoveryEntry( + definition: LimeCapabilityDefinitionRecord, + profile: HostCapabilityProfile, +): CapabilityDiscoveryEntry { + const support = resolveCapabilityProfileSupport(definition, profile); + const implementation = support?.implementation ?? "none"; + const enabled = support?.enabled === true && implementation !== "none"; + const unavailableReason = enabled + ? undefined + : String(definition.stage) === "planned" + ? "planned" + : implementation === "none" + ? "not_implemented" + : "disabled"; + + const entry: CapabilityDiscoveryEntry = { + name: definition.name, + version: support?.version ?? definition.version, + group: definition.group, + stage: definition.stage, + owner: definition.owner, + methods: [...definition.methods], + summary: definition.summary, + enabled, + implementation, + }; + return unavailableReason ? { ...entry, unavailableReason } : entry; +} + +function resolveCapabilityProfileSupport( + definition: LimeCapabilityDefinitionRecord, + profile: HostCapabilityProfile, +): CapabilityProfileSupport | undefined { + const support = profile.capabilities[definition.name]; + if (definition.name !== "lime.capabilities") { + return support; + } + return { + version: support?.version ?? definition.version, + enabled: true, + implementation: "native", + }; +} + +function readAgentRuntimeTaskContract( + agentRuntime: unknown, +): Record { + if (!isRecord(agentRuntime)) { + return {}; + } + const task = isRecord(agentRuntime.agentTask) + ? agentRuntime.agentTask + : isRecord(agentRuntime.agent_task) + ? agentRuntime.agent_task + : isRecord(agentRuntime.task) + ? agentRuntime.task + : {}; + return { + eventSchema: readString(task.eventSchema), + resultSchema: readString(task.resultSchema), + structuredOutput: + isRecord(task.structuredOutput) || + isRecord(agentRuntime.structuredOutput), + approval: isRecord(task.approval) || isRecord(agentRuntime.approval), + sessionPolicy: + isRecord(task.sessionPolicy) || isRecord(agentRuntime.sessionPolicy), + toolDiscovery: + isRecord(task.toolDiscovery) || isRecord(agentRuntime.toolDiscovery), + checkpointScope: + isRecord(task.checkpointScope) || isRecord(agentRuntime.checkpointScope), + observability: + isRecord(task.observability) || isRecord(agentRuntime.observability), + }; +} + +function countLayerItems(value: unknown, itemKey: string): number { + if (Array.isArray(value)) { + return value.length; + } + if (!isRecord(value)) { + return 0; + } + const items = value[itemKey]; + return Array.isArray(items) ? items.length : 0; +} + +function hasLayerValue(value: unknown, itemKey: string): boolean { + if (countLayerItems(value, itemKey) > 0) { + return true; + } + return isRecord(value) && Object.keys(value).length > 0; +} + +function isManifestStandardVersion( + manifestVersion: string | undefined, + standardVersion: string, +): boolean { + return ( + manifestVersion === standardVersion || + Boolean(manifestVersion?.startsWith(`${standardVersion}.`)) + ); +} + +function readCapabilityHandoffContract(params: { + manifestVersion?: string; + requirements?: unknown; + boundary?: unknown; + integrations?: unknown; + operations?: unknown; +}): Record { + const requirementCount = countLayerItems(params.requirements, "requirements"); + const boundaryCount = countLayerItems(params.boundary, "boundaries"); + const integrationCount = countLayerItems(params.integrations, "integrations"); + const operationCount = countLayerItems(params.operations, "operations"); + const hasCapabilityHandoff = + isManifestStandardVersion(params.manifestVersion, "0.7") || + hasLayerValue(params.requirements, "requirements") || + hasLayerValue(params.boundary, "boundaries") || + hasLayerValue(params.integrations, "integrations") || + hasLayerValue(params.operations, "operations"); + + return { + version: "0.7", + enabled: hasCapabilityHandoff, + manifestVersion: params.manifestVersion, + layerFiles: [ + "app.requirements.yaml", + "app.boundary.yaml", + "app.integrations.yaml", + "app.operations.yaml", + ], + requirementCount, + boundaryCount, + integrationCount, + operationCount, + hostCloudManagedExecution: true, + externalSideEffectsRequireApproval: true, + appCredentialsBoundary: "host_or_cloud_managed", + }; +} + +function buildAgentAppStandardProfile(params: { + manifestVersion?: string; + agentRuntime?: unknown; + requirements?: unknown; + boundary?: unknown; + integrations?: unknown; + operations?: unknown; +}): Record { + const hasAgentRuntime = isRecord(params.agentRuntime); + return { + layeredManifest: { + version: "0.5", + enabled: true, + layerFiles: [ + "app.capabilities.yaml", + "app.entries.yaml", + "app.permissions.yaml", + "app.errors.yaml", + "app.i18n.yaml", + "app.signature.yaml", + "evals/readiness.yaml", + "evals/health.yaml", + ], + }, + agentRuntime: { + version: "0.6", + enabled: hasAgentRuntime, + manifestVersion: params.manifestVersion, + layerFiles: ["app.runtime.yaml"], + ...readAgentRuntimeTaskContract(params.agentRuntime), + }, + requirementBoundary: readCapabilityHandoffContract(params), + }; +} + +function dispatchCapabilities( + request: AgentAppHostBridgeCapabilityRequest, + profile: HostCapabilityProfile, + standardProfile: { + manifestVersion?: string; + agentRuntime?: unknown; + requirements?: unknown; + boundary?: unknown; + integrations?: unknown; + operations?: unknown; + } = {}, +): unknown { + if (request.method === "list") { + return LIME_CAPABILITY_DEFINITIONS.map((definition) => + buildCapabilityDiscoveryEntry(definition, profile), + ); + } + if (request.method === "get") { + const capability = readStringParam(request, "capability", 0); + return buildCapabilityDiscoveryEntry( + resolveCapabilityDefinition(capability), + profile, + ); + } + if (request.method === "getProfile") { + const agentRuntime = standardProfile.agentRuntime ?? profile.agentRuntime; + const payload: Record = { + appRuntimeVersion: profile.appRuntimeVersion, + standardVersions: profile.standardVersions ?? { + current: currentAgentAppStandardVersion, + compatible: [...compatibleAgentAppStandardVersions], + }, + runtimeTargets: [...profile.runtimeTargets], + capabilities: Object.fromEntries( + LIME_CAPABILITY_DEFINITIONS.map((definition) => [ + definition.name, + buildCapabilityDiscoveryEntry(definition, profile), + ]), + ), + standards: buildAgentAppStandardProfile({ + manifestVersion: standardProfile.manifestVersion, + agentRuntime, + requirements: standardProfile.requirements, + boundary: standardProfile.boundary, + integrations: standardProfile.integrations, + operations: standardProfile.operations, + }), + featureFlags: { ...profile.featureFlags }, + }; + if (agentRuntime !== undefined) { + payload.agentRuntime = agentRuntime; + } + if (standardProfile.requirements !== undefined) { + payload.requirements = standardProfile.requirements; + } + if (standardProfile.boundary !== undefined) { + payload.boundary = standardProfile.boundary; + } + if (standardProfile.integrations !== undefined) { + payload.integrations = standardProfile.integrations; + } + if (standardProfile.operations !== undefined) { + payload.operations = standardProfile.operations; + } + return payload; + } + throwUnsupportedMethod(request); +} + +function readTaskRuntimeProcess(task: AgentAppTaskRecord) { + return task.runtimeProcess ?? task.process ?? null; +} + +function hasRoutedModel( + model: AgentAppRuntimeProcessModel | null | undefined, +): model is AgentAppRuntimeProcessModel { + if (!model) { + return false; + } + return Boolean(model.provider || model.model); +} + +function numberValue(value: unknown): number | undefined { + const number = Number(value); + return Number.isFinite(number) ? number : undefined; +} + +function integerValue(value: unknown): number { + const number = numberValue(value); + return number === undefined ? 0 : Math.max(0, Math.floor(number)); +} + +function recordString( + record: Record | null | undefined, + key: string, +): string | undefined { + return record ? readString(record[key]) : undefined; +} + +function recordArray( + record: Record | null | undefined, + key: string, +): unknown[] { + const value = record?.[key]; + return Array.isArray(value) ? value : []; +} + +function sortTasksByNewest(tasks: AgentAppTaskRecord[]): AgentAppTaskRecord[] { + return [...tasks].sort((left, right) => + String(right.finishedAt ?? right.startedAt).localeCompare( + String(left.finishedAt ?? left.startedAt), + ), + ); +} + +function filterRuntimeProjectionTasks( + host: CapabilityHost, + request: AgentAppHostBridgeCapabilityRequest, +): AgentAppTaskRecord[] { + const input = readOptionalInputRecord(request); + const taskId = + readString(input.taskId) ?? readOptionalStringParam(request, "taskId", 0); + const taskKind = readString(input.taskKind); + return sortTasksByNewest( + host + .getTasks({ + appId: request.appId, + entryKey: request.entryKey, + }) + .filter((task) => !taskId || task.taskId === taskId) + .filter((task) => !taskKind || task.taskKind === taskKind), + ); +} + +function buildModelProjection( + task: AgentAppTaskRecord, +): RuntimeModelProjection | null { + const process = readTaskRuntimeProcess(task); + if (!hasRoutedModel(process?.model)) { + return null; + } + return { + taskId: task.taskId, + taskKind: task.taskKind, + status: task.status, + startedAt: task.startedAt, + finishedAt: task.finishedAt, + model: process.model, + }; +} + +function buildUsageProjection( + task: AgentAppTaskRecord, +): RuntimeUsageProjection | null { + const process = readTaskRuntimeProcess(task); + if (!process?.usage) { + return null; + } + return { + taskId: task.taskId, + taskKind: task.taskKind, + status: task.status, + startedAt: task.startedAt, + finishedAt: task.finishedAt, + usage: process.usage, + model: process.model, + }; +} + +function buildCostProjection( + task: AgentAppTaskRecord, +): RuntimeCostProjection | null { + const process = readTaskRuntimeProcess(task); + if (!process?.cost) { + return null; + } + return { + taskId: task.taskId, + taskKind: task.taskKind, + status: task.status, + startedAt: task.startedAt, + finishedAt: task.finishedAt, + cost: process.cost, + model: process.model, + }; +} + +function aggregateUsage( + items: RuntimeUsageProjection[], +): AgentAppRuntimeProcessUsage { + return items.reduce( + (total, item) => ({ + inputTokens: + total.inputTokens + + (numberValue(item.usage.inputTokens ?? item.usage.input_tokens) ?? 0), + outputTokens: + total.outputTokens + + (numberValue(item.usage.outputTokens ?? item.usage.output_tokens) ?? 0), + totalTokens: + total.totalTokens + + (numberValue(item.usage.totalTokens ?? item.usage.total_tokens) ?? 0), + cachedInputTokens: + (total.cachedInputTokens ?? 0) + + (numberValue( + item.usage.cachedInputTokens ?? item.usage.cached_input_tokens, + ) ?? 0), + cacheCreationInputTokens: + (total.cacheCreationInputTokens ?? 0) + + (numberValue( + item.usage.cacheCreationInputTokens ?? + item.usage.cache_creation_input_tokens, + ) ?? 0), + }), + { + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + cachedInputTokens: 0, + cacheCreationInputTokens: 0, + }, + ); +} + +function aggregateCost(items: RuntimeCostProjection[]) { + const costs = items + .map((item) => + numberValue( + item.cost.estimatedTotalCost ?? + item.cost.estimated_total_cost ?? + item.cost.totalCost ?? + item.cost.total_cost, + ), + ) + .filter((value): value is number => value !== undefined); + return { + estimatedTotalCost: costs.reduce((total, value) => total + value, 0), + currency: readString(items[0]?.cost.currency) ?? "unknown", + }; +} + +function uniqueModelSummaries(items: RuntimeModelProjection[]) { + const summaries = new Map< + string, + AgentAppRuntimeProcessModel & { + taskCount: number; + taskKinds: string[]; + lastTaskId: string; + lastSeenAt: string; + } + >(); + items.forEach((item) => { + const key = `${item.model.provider}\u0000${item.model.model}\u0000${item.model.label}`; + const existing = summaries.get(key); + if (existing) { + existing.taskCount += 1; + existing.taskKinds = Array.from( + new Set([...existing.taskKinds, item.taskKind]), + ).sort(); + if (String(item.finishedAt ?? item.startedAt) > existing.lastSeenAt) { + existing.lastTaskId = item.taskId; + existing.lastSeenAt = String(item.finishedAt ?? item.startedAt); + } + return; + } + summaries.set(key, { + ...item.model, + taskCount: 1, + taskKinds: [item.taskKind], + lastTaskId: item.taskId, + lastSeenAt: String(item.finishedAt ?? item.startedAt), + }); + }); + return Array.from(summaries.values()).sort((left, right) => + right.lastSeenAt.localeCompare(left.lastSeenAt), + ); +} + +function normalizeSkillName(value: string): string { + return value.trim(); +} + +function buildRuntimeSkillProjection( + tasks: AgentAppTaskRecord[], +): RuntimeSkillProjection[] { + const summaries = new Map(); + tasks.forEach((task) => { + const process = readTaskRuntimeProcess(task); + const declared = new Set( + (process?.skillNames ?? []).map(normalizeSkillName).filter(Boolean), + ); + const invoked = new Set( + (process?.invokedSkillNames ?? []) + .map(normalizeSkillName) + .filter(Boolean), + ); + new Set([...declared, ...invoked]).forEach((name) => { + const existing = summaries.get(name); + const lastSeenAt = String(task.finishedAt ?? task.startedAt); + const status = invoked.has(name) ? "invoked" : "declared"; + if (existing) { + existing.taskCount += 1; + existing.invocationCount += invoked.has(name) ? 1 : 0; + existing.status = + existing.status === "invoked" || status === "invoked" + ? "invoked" + : "declared"; + existing.taskIds = Array.from( + new Set([...existing.taskIds, task.taskId]), + ); + existing.taskKinds = Array.from( + new Set([...existing.taskKinds, task.taskKind]), + ).sort(); + if (lastSeenAt > existing.lastSeenAt) { + existing.lastSeenAt = lastSeenAt; + } + return; + } + summaries.set(name, { + skillId: name, + name, + status, + taskCount: 1, + invocationCount: invoked.has(name) ? 1 : 0, + taskIds: [task.taskId], + taskKinds: [task.taskKind], + lastSeenAt, + source: "agent_runtime_process", + }); + }); + }); + return Array.from(summaries.values()).sort((left, right) => + right.lastSeenAt.localeCompare(left.lastSeenAt), + ); +} + +function buildRuntimeSkillInvocations( + tasks: AgentAppTaskRecord[], +): RuntimeSkillInvocationProjection[] { + return tasks.flatMap((task) => { + const process = readTaskRuntimeProcess(task); + return (process?.invokedSkillNames ?? []) + .map(normalizeSkillName) + .filter(Boolean) + .map((name) => ({ + invocationId: `${task.taskId}:${name}`, + skillId: name, + name, + taskId: task.taskId, + taskKind: task.taskKind, + status: task.status, + startedAt: task.startedAt, + finishedAt: task.finishedAt, + source: "agent_runtime_process" as const, + })); + }); +} + +const TOOL_INTEGRATION_SPECS: Record< + ToolIntegrationCapability, + { + keywords: string[]; + toolHints: string[]; + reason: string; + } +> = { + "lime.search": { + keywords: ["search", "websearch", "research", "deepresearch", "citation"], + toolHints: ["lime.capability.research.search", "web_search"], + reason: "search_execution_requires_lime_agent_task", + }, + "lime.browser": { + keywords: [ + "browser", + "chrome", + "webpage", + "readpage", + "screenshot", + "navigate", + ], + toolHints: ["browser", "read_page", "screenshot"], + reason: "browser_runtime_execution_requires_lime_tool_runtime_policy", + }, + "lime.documents": { + keywords: ["document", "pdf", "docx", "word", "markdown", "ppt", "pptx"], + toolHints: ["document_parser", "pdf.read"], + reason: "document_runtime_execution_requires_lime_tool_runtime_policy", + }, + "lime.media": { + keywords: [ + "media", + "image", + "audio", + "voice", + "video", + "transcribe", + "synthesize", + "tts", + ], + toolHints: ["image_generation", "audio_transcription", "voice_synthesis"], + reason: "media_runtime_execution_requires_lime_tool_runtime_policy", + }, + "lime.mcp": { + keywords: ["mcp", "mcpserver", "mcp__"], + toolHints: ["mcp__server__tool"], + reason: "mcp_execution_requires_lime_tool_runtime_policy", + }, + "lime.terminal": { + keywords: ["terminal", "shell", "command", "bash", "powershell", "cmd"], + toolHints: ["terminal.run"], + reason: "terminal_execution_requires_lime_sandbox_policy", + }, + "lime.connectors": { + keywords: ["connector", "connectors", "integration", "notion", "slack"], + toolHints: ["connector.invoke"], + reason: "connector_execution_requires_lime_policy_and_secret_binding", + }, +}; + +const TOOL_INTENT_KEYS = new Set([ + "action", + "artifactId", + "command", + "connectorId", + "cwdRef", + "depth", + "format", + "fullPage", + "input", + "limit", + "operation", + "options", + "prompt", + "quality", + "query", + "ref", + "reason", + "runId", + "selector", + "serverId", + "sessionId", + "size", + "style", + "text", + "tool", + "url", + "voice", +]); + +function classifyToolIntegrationCapability( + value: string, +): ToolIntegrationCapability | null { + const token = capabilityMatchToken(value); + if (!token) { + return null; + } + if (token.includes("mcp")) { + return "lime.mcp"; + } + if ( + token.includes("terminal") || + token.includes("shell") || + token.includes("powershell") + ) { + return "lime.terminal"; + } + if (token.includes("connector")) { + return "lime.connectors"; + } + const capability = ( + Object.entries(TOOL_INTEGRATION_SPECS) as Array< + [ + ToolIntegrationCapability, + (typeof TOOL_INTEGRATION_SPECS)[ToolIntegrationCapability], + ] + > + ).find(([, spec]) => + spec.keywords.some((keyword) => + token.includes(capabilityMatchToken(keyword)), + ), + )?.[0]; + return capability ?? null; +} + +function normalizeToolIntegrationName(value: string): string { + return value + .replace(/^Tool\s*[·:]\s*/i, "") + .replace(/^执行参数流\s*[·:]\s*/i, "") + .replace(/^执行结果流\s*[·:]\s*/i, "") + .trim(); +} + +function buildDeclaredToolRun( + task: AgentAppTaskRecord, + toolName: string, +): RuntimeToolRunProjection | null { + const capability = classifyToolIntegrationCapability(toolName); + if (!capability) { + return null; + } + return { + runId: `${task.taskId}:${capability}:${capabilityMatchToken(toolName)}:declared`, + capability, + toolName, + taskId: task.taskId, + taskKind: task.taskKind, + status: "declared", + startedAt: task.startedAt, + finishedAt: task.finishedAt, + title: `Tool intent · ${toolName}`, + statusText: "已声明", + message: + "Agent App task 声明了该 ToolRuntime intent,实际执行仍由 Lime AgentRuntime 管理。", + source: "agent_runtime_process", + }; +} + +function buildTimelineToolRun( + task: AgentAppTaskRecord, + item: AgentAppRuntimeProcessTimelineItem, + index: number, +): RuntimeToolRunProjection | null { + const surface = [item.title, item.message, item.detail, item.meta] + .filter(Boolean) + .join(" "); + const capability = classifyToolIntegrationCapability(surface); + if (!capability) { + return null; + } + const toolName = normalizeToolIntegrationName( + item.title || item.meta || surface, + ); + return { + runId: + readString(item.meta) ?? + `${task.taskId}:${capability}:${capabilityMatchToken(toolName)}:${index}`, + capability, + toolName, + taskId: task.taskId, + taskKind: task.taskKind, + status: task.status === "running" ? "observed" : task.status, + startedAt: task.startedAt, + finishedAt: task.finishedAt, + title: item.title, + statusText: item.statusText, + message: item.message, + detail: item.detail, + source: "agent_runtime_process", + }; +} + +function buildRuntimeToolRuns( + tasks: AgentAppTaskRecord[], + capability?: ToolIntegrationCapability, +): RuntimeToolRunProjection[] { + const runs = tasks.flatMap((task) => { + const process = readTaskRuntimeProcess(task); + const declared = task.tools + .map((toolName) => buildDeclaredToolRun(task, toolName)) + .filter((item): item is RuntimeToolRunProjection => Boolean(item)); + const observed = (process?.timeline ?? []) + .filter((item) => item.kind === "tool" || item.kind === "execution") + .map((item, index) => buildTimelineToolRun(task, item, index)) + .filter((item): item is RuntimeToolRunProjection => Boolean(item)); + return [...observed, ...declared]; + }); + return runs + .filter((run) => !capability || run.capability === capability) + .sort((left, right) => + String(right.finishedAt ?? right.startedAt).localeCompare( + String(left.finishedAt ?? left.startedAt), + ), + ); +} + +function readToolIntent( + input: Record, +): Record { + return Object.fromEntries( + Object.entries(input).filter(([key]) => TOOL_INTENT_KEYS.has(key)), + ); +} + +function buildToolIntentResponse( + request: AgentAppHostBridgeCapabilityRequest, + capability: ToolIntegrationCapability, + input: Record, + runs: RuntimeToolRunProjection[], +): unknown { + const spec = TOOL_INTEGRATION_SPECS[capability]; + return { + appId: request.appId, + capability, + method: request.method, + status: "requires_agent_task", + reason: spec.reason, + source: "tool_runtime_policy", + intent: readToolIntent(input), + toolHints: spec.toolHints, + matchingRuns: runs, + next: { + capability: "lime.agent", + method: "startTask", + reason: "actual_tool_execution_is_owned_by_lime_agent_runtime", + }, + }; +} + +function readToolRun( + request: AgentAppHostBridgeCapabilityRequest, + runs: RuntimeToolRunProjection[], +): RuntimeToolRunProjection { + const runId = readStringParam(request, "runId", 0); + const run = runs.find( + (item) => item.runId === runId || item.taskId === runId, + ); + if (!run) { + throw new AgentAppCapabilityDispatcherError( + "TOOL_RUN_NOT_FOUND", + `${runId} was not found in AgentRuntime tool projection.`, + ); + } + return run; +} + +function parseMcpToolName(toolName: string): { + serverId: string; + toolId: string; +} { + const normalized = toolName + .replace(/^Tool\s*[·:]\s*/i, "") + .replace(/^mcp[:./-]/i, "mcp__") + .trim(); + const match = /^mcp__([^_]+)__(.+)$/i.exec(normalized); + if (match) { + return { + serverId: match[1], + toolId: match[2], + }; + } + return { + serverId: "unknown", + toolId: normalized || toolName, + }; +} + +function buildRuntimeMcpTools( + runs: RuntimeToolRunProjection[], +): RuntimeMcpToolProjection[] { + const tools = new Map(); + runs.forEach((run) => { + const parsed = parseMcpToolName(run.toolName); + const key = `${parsed.serverId}\u0000${parsed.toolId}`; + const lastSeenAt = String(run.finishedAt ?? run.startedAt); + const existing = tools.get(key); + if (existing) { + existing.runIds = Array.from(new Set([...existing.runIds, run.runId])); + existing.taskIds = Array.from(new Set([...existing.taskIds, run.taskId])); + if (lastSeenAt > existing.lastSeenAt) { + existing.lastSeenAt = lastSeenAt; + } + return; + } + tools.set(key, { + toolName: run.toolName, + serverId: parsed.serverId, + toolId: parsed.toolId, + runIds: [run.runId], + taskIds: [run.taskId], + lastSeenAt, + source: "agent_runtime_process", + }); + }); + return Array.from(tools.values()).sort((left, right) => + right.lastSeenAt.localeCompare(left.lastSeenAt), + ); +} + +function parseConnectorToolName(toolName: string): { + connectorId: string; + actionId?: string; +} { + const normalized = toolName + .replace(/^Tool\s*[·:]\s*/i, "") + .replace(/^connector[:./-]/i, "connector__") + .trim(); + const match = /^connector__([^_]+)__(.+)$/i.exec(normalized); + if (match) { + return { + connectorId: match[1], + actionId: match[2], + }; + } + return { + connectorId: normalized || toolName, + }; +} + +function buildRuntimeConnectors( + runs: RuntimeToolRunProjection[], +): RuntimeConnectorProjection[] { + const connectors = new Map(); + runs.forEach((run) => { + const parsed = parseConnectorToolName(run.toolName); + const connectorId = parsed.connectorId; + const lastSeenAt = String(run.finishedAt ?? run.startedAt); + const existing = connectors.get(connectorId); + if (existing) { + existing.actionIds = Array.from( + new Set( + parsed.actionId + ? [...existing.actionIds, parsed.actionId] + : existing.actionIds, + ), + ).sort(); + existing.runIds = Array.from(new Set([...existing.runIds, run.runId])); + existing.taskIds = Array.from(new Set([...existing.taskIds, run.taskId])); + if (lastSeenAt > existing.lastSeenAt) { + existing.lastSeenAt = lastSeenAt; + } + return; + } + connectors.set(connectorId, { + connectorId, + actionIds: parsed.actionId ? [parsed.actionId] : [], + runIds: [run.runId], + taskIds: [run.taskId], + lastSeenAt, + source: "agent_runtime_process", + }); + }); + return Array.from(connectors.values()).sort((left, right) => + right.lastSeenAt.localeCompare(left.lastSeenAt), + ); +} + +function readTaskThreadRead( + task: AgentAppTaskRecord, +): Record | null { + if (!isRecord(task.result)) { + return null; + } + return isRecord(task.result.threadRead) + ? task.result.threadRead + : task.result; +} + +function readTaskThreadDiagnostics( + task: AgentAppTaskRecord, +): Record | null { + const threadRead = readTaskThreadRead(task); + return isRecord(threadRead?.diagnostics) ? threadRead.diagnostics : null; +} + +function readKnowledgeBindingKeys(task: AgentAppTaskRecord): string[] { + return task.knowledge + .map((binding) => binding.key.trim()) + .filter(Boolean) + .sort(); +} + +function readThreadTurnIds( + threadRead: Record | null, +): string[] { + return recordArray(threadRead, "turns") + .filter(isRecord) + .map( + (turn) => + recordString(turn, "turn_id") ?? + recordString(turn, "turnId") ?? + recordString(turn, "id"), + ) + .filter((item): item is string => Boolean(item)); +} + +function buildRuntimeMemoryProjection( + task: AgentAppTaskRecord, +): RuntimeMemoryProjection { + const diagnostics = readTaskThreadDiagnostics(task); + return { + taskId: task.taskId, + taskKind: task.taskKind, + status: task.status, + startedAt: task.startedAt, + finishedAt: task.finishedAt, + scope: "task", + knowledgeBindingKeys: readKnowledgeBindingKeys(task), + contextCompactionCount: integerValue( + diagnostics?.context_compaction_count ?? + diagnostics?.contextCompactionCount, + ), + pendingRequestCount: integerValue( + diagnostics?.pending_request_count ?? diagnostics?.pendingRequestCount, + ), + source: "agent_runtime_projection", + }; +} + +function buildRuntimeContextProjection( + task: AgentAppTaskRecord, +): RuntimeContextProjection { + const threadRead = readTaskThreadRead(task); + const diagnostics = readTaskThreadDiagnostics(task); + return { + taskId: task.taskId, + traceId: task.traceId, + taskKind: task.taskKind, + status: task.status, + startedAt: task.startedAt, + finishedAt: task.finishedAt, + workspaceId: task.provenance.workspaceId, + threadId: + recordString(threadRead, "thread_id") ?? + recordString(threadRead, "threadId"), + turnIds: readThreadTurnIds(threadRead), + knowledgeBindingKeys: readKnowledgeBindingKeys(task), + toolKeys: [...task.tools].sort(), + fileRefs: [...task.files].sort(), + inputAttached: task.input !== undefined, + expectedOutputAttached: task.expectedOutput !== undefined, + pendingRequestCount: integerValue( + diagnostics?.pending_request_count ?? diagnostics?.pendingRequestCount, + ), + source: "agent_runtime_projection", + }; +} + +function dispatchModels( + host: CapabilityHost, + request: AgentAppHostBridgeCapabilityRequest, +): unknown { + const tasks = filterRuntimeProjectionTasks(host, request); + const routedTasks = tasks + .map(buildModelProjection) + .filter((item): item is RuntimeModelProjection => Boolean(item)); + if (request.method === "list") { + return { + appId: request.appId, + source: "agent_runtime_projection", + taskCount: tasks.length, + models: uniqueModelSummaries(routedTasks), + }; + } + if (request.method === "getRouting") { + return { + appId: request.appId, + source: "agent_runtime_projection", + taskCount: tasks.length, + routes: routedTasks, + }; + } + if (request.method === "select") { + const selected = routedTasks[0]; + return selected + ? { + status: "selected", + source: "latest_runtime_projection", + selected, + } + : { + status: "unavailable", + source: "latest_runtime_projection", + reason: "no_runtime_routing_facts", + }; + } + if (request.method === "estimateCost") { + const costs = tasks + .map(buildCostProjection) + .filter((item): item is RuntimeCostProjection => Boolean(item)); + return { + appId: request.appId, + status: costs.length + ? "estimated_from_runtime_projection" + : "insufficient_data", + source: "agent_runtime_projection", + sampleSize: costs.length, + cost: aggregateCost(costs), + }; + } + throwUnsupportedMethod(request); +} + +function dispatchSkills( + host: CapabilityHost, + request: AgentAppHostBridgeCapabilityRequest, +): unknown { + const input = readOptionalInputRecord(request); + const tasks = filterRuntimeProjectionTasks(host, request); + const skills = buildRuntimeSkillProjection(tasks); + if (request.method === "list") { + const kind = readString(input.kind); + return { + appId: request.appId, + source: "agent_runtime_process", + taskCount: tasks.length, + skills: kind + ? skills.filter( + (skill) => skill.status === kind || skill.source === kind, + ) + : skills, + }; + } + if (request.method === "resolve") { + const skillId = readStringParam(request, "skillId", 0); + const skill = skills.find((item) => item.skillId === skillId); + if (!skill) { + throw new AgentAppCapabilityDispatcherError( + "SKILL_NOT_FOUND", + `${skillId} was not found in AgentRuntime process projection.`, + ); + } + return skill; + } + if (request.method === "getInvocation") { + const invocationId = readStringParam(request, "invocationId", 0); + const invocation = buildRuntimeSkillInvocations(tasks).find( + (item) => item.invocationId === invocationId, + ); + if (!invocation) { + throw new AgentAppCapabilityDispatcherError( + "SKILL_INVOCATION_NOT_FOUND", + `${invocationId} was not found in AgentRuntime process projection.`, + ); + } + return invocation; + } + if (request.method === "bind" || request.method === "invoke") { + return { + status: "not_available", + reason: "skill_runtime_mutation_not_exposed_to_agent_apps", + source: "agent_runtime_process", + }; + } + throwUnsupportedMethod(request); +} + +function dispatchMemory( + host: CapabilityHost, + request: AgentAppHostBridgeCapabilityRequest, +): unknown { + const input = readOptionalInputRecord(request); + const tasks = filterRuntimeProjectionTasks(host, request); + const observations = tasks.map(buildRuntimeMemoryProjection); + if (request.method === "getStatus") { + return { + appId: request.appId, + scope: readString(input.scope) ?? "task", + status: "read_only_projection", + source: "agent_runtime_projection", + taskCount: tasks.length, + writable: false, + compactable: false, + totals: { + knowledgeBindingCount: observations.reduce( + (total, item) => total + item.knowledgeBindingKeys.length, + 0, + ), + contextCompactionCount: observations.reduce( + (total, item) => total + item.contextCompactionCount, + 0, + ), + pendingRequestCount: observations.reduce( + (total, item) => total + item.pendingRequestCount, + 0, + ), + }, + observations, + }; + } + if (request.method === "query") { + const query = readStringParam(request, "query", 0).toLowerCase(); + return { + appId: request.appId, + query, + status: "limited_projection", + source: "agent_runtime_projection", + records: observations.filter((item) => + [item.taskId, item.taskKind, ...item.knowledgeBindingKeys] + .join(" ") + .toLowerCase() + .includes(query), + ), + }; + } + if (request.method === "write" || request.method === "compact") { + return { + status: "not_available", + reason: "memory_runtime_mutation_not_exposed_to_agent_apps", + source: "agent_runtime_projection", + }; + } + throwUnsupportedMethod(request); +} + +function dispatchContext( + host: CapabilityHost, + request: AgentAppHostBridgeCapabilityRequest, +): unknown { + const input = readOptionalInputRecord(request); + const tasks = filterRuntimeProjectionTasks(host, request); + if (request.method === "getSnapshot") { + return { + appId: request.appId, + scope: readString(input.scope) ?? "task", + source: "agent_runtime_projection", + taskCount: tasks.length, + contexts: tasks.map(buildRuntimeContextProjection), + }; + } + if (request.method === "attach" || request.method === "detach") { + return { + status: "not_available", + reason: "context_mutation_not_exposed_to_agent_apps", + source: "agent_runtime_projection", + }; + } + throwUnsupportedMethod(request); +} + +function dispatchSearch( + host: CapabilityHost, + request: AgentAppHostBridgeCapabilityRequest, +): unknown { + const input = readOptionalInputRecord(request); + const tasks = filterRuntimeProjectionTasks(host, request); + const runs = buildRuntimeToolRuns(tasks, "lime.search"); + if (request.method === "getRun") { + return readToolRun(request, runs); + } + if (request.method === "query" || request.method === "deepResearch") { + readStringParam(request, "query", 0); + return buildToolIntentResponse(request, "lime.search", input, runs); + } + throwUnsupportedMethod(request); +} + +function dispatchBrowser( + host: CapabilityHost, + request: AgentAppHostBridgeCapabilityRequest, +): unknown { + const input = readOptionalInputRecord(request); + const tasks = filterRuntimeProjectionTasks(host, request); + const runs = buildRuntimeToolRuns(tasks, "lime.browser"); + if (request.method === "navigate") { + readStringParam(request, "sessionId", 0); + readStringParam(request, "url", 1); + } else if ( + request.method === "extract" || + request.method === "screenshot" || + request.method === "close" + ) { + readStringParam(request, "sessionId", 0); + } else if (request.method !== "open") { + throwUnsupportedMethod(request); + } + return buildToolIntentResponse(request, "lime.browser", input, runs); +} + +function dispatchDocuments( + host: CapabilityHost, + request: AgentAppHostBridgeCapabilityRequest, +): unknown { + const input = readOptionalInputRecord(request); + const tasks = filterRuntimeProjectionTasks(host, request); + const runs = buildRuntimeToolRuns(tasks, "lime.documents"); + if (request.method === "parse" || request.method === "summarize") { + readStringParam(request, "ref", 0); + } else if (request.method === "export") { + readStringParam(request, "artifactId", 0); + readStringParam(request, "format", 1); + } else if (request.method === "transform") { + readStringParam(request, "ref", 0); + readStringParam(request, "operation", 1); + } else { + throwUnsupportedMethod(request); + } + return buildToolIntentResponse(request, "lime.documents", input, runs); +} + +function dispatchMedia( + host: CapabilityHost, + request: AgentAppHostBridgeCapabilityRequest, +): unknown { + const input = readOptionalInputRecord(request); + const tasks = filterRuntimeProjectionTasks(host, request); + const runs = buildRuntimeToolRuns(tasks, "lime.media"); + if (request.method === "generateImage") { + readStringParam(request, "prompt", 0); + } else if (request.method === "editImage") { + readStringParam(request, "ref", 0); + readStringParam(request, "prompt", 1); + } else if (request.method === "transcribe") { + readStringParam(request, "ref", 0); + } else if (request.method === "synthesizeVoice") { + readStringParam(request, "text", 0); + } else { + throwUnsupportedMethod(request); + } + return buildToolIntentResponse(request, "lime.media", input, runs); +} + +function dispatchMcp( + host: CapabilityHost, + request: AgentAppHostBridgeCapabilityRequest, +): unknown { + const input = readOptionalInputRecord(request); + const tasks = filterRuntimeProjectionTasks(host, request); + const runs = buildRuntimeToolRuns(tasks, "lime.mcp"); + const tools = buildRuntimeMcpTools(runs); + if (request.method === "listServers") { + const servers = new Map< + string, + { + serverId: string; + toolCount: number; + runIds: string[]; + lastSeenAt: string; + source: "agent_runtime_process"; + } + >(); + tools.forEach((tool) => { + const existing = servers.get(tool.serverId); + if (existing) { + existing.toolCount += 1; + existing.runIds = Array.from( + new Set([...existing.runIds, ...tool.runIds]), + ); + if (tool.lastSeenAt > existing.lastSeenAt) { + existing.lastSeenAt = tool.lastSeenAt; + } + return; + } + servers.set(tool.serverId, { + serverId: tool.serverId, + toolCount: 1, + runIds: [...tool.runIds], + lastSeenAt: tool.lastSeenAt, + source: "agent_runtime_process", + }); + }); + return { + appId: request.appId, + status: "read_only_projection", + source: "agent_runtime_process", + servers: Array.from(servers.values()).sort((left, right) => + right.lastSeenAt.localeCompare(left.lastSeenAt), + ), + }; + } + if (request.method === "listTools") { + const serverId = readString(input.serverId); + return { + appId: request.appId, + status: "read_only_projection", + source: "agent_runtime_process", + tools: serverId + ? tools.filter((tool) => tool.serverId === serverId) + : tools, + }; + } + if (request.method === "invoke") { + readStringParam(request, "tool", 0); + return buildToolIntentResponse(request, "lime.mcp", input, runs); + } + throwUnsupportedMethod(request); +} + +function dispatchTerminal( + host: CapabilityHost, + request: AgentAppHostBridgeCapabilityRequest, +): unknown { + const input = readOptionalInputRecord(request); + const tasks = filterRuntimeProjectionTasks(host, request); + const runs = buildRuntimeToolRuns(tasks, "lime.terminal"); + if (request.method === "getRun") { + return readToolRun(request, runs); + } + if (request.method === "run") { + readStringParam(request, "command", 0); + return buildToolIntentResponse(request, "lime.terminal", input, runs); + } + if (request.method === "cancel") { + return { + status: "not_available", + reason: "terminal_runtime_cancellation_not_exposed_to_agent_apps", + source: "tool_runtime_policy", + }; + } + throwUnsupportedMethod(request); +} + +function dispatchConnectors( + host: CapabilityHost, + request: AgentAppHostBridgeCapabilityRequest, +): unknown { + const input = readOptionalInputRecord(request); + const tasks = filterRuntimeProjectionTasks(host, request); + const runs = buildRuntimeToolRuns(tasks, "lime.connectors"); + const connectors = buildRuntimeConnectors(runs); + if (request.method === "list") { + return { + appId: request.appId, + kind: readString(input.kind), + status: "read_only_projection", + source: "agent_runtime_process", + connectors, + }; + } + if (request.method === "getStatus") { + const connectorId = readStringParam(request, "connectorId", 0); + const connector = connectors.find( + (item) => item.connectorId === connectorId, + ); + return connector + ? { + connectorId, + status: "observed", + source: "agent_runtime_process", + connector, + } + : { + connectorId, + status: "not_connected", + reason: "no_connector_runtime_facts", + source: "agent_runtime_process", + }; + } + if (request.method === "requestAuth") { + readStringParam(request, "connectorId", 0); + return { + appId: request.appId, + capability: "lime.connectors", + method: request.method, + status: "requires_host_authorization", + reason: "connector_auth_requires_lime_policy_and_secret_binding", + source: "tool_runtime_policy", + intent: readToolIntent(input), + }; + } + if (request.method === "invoke") { + readStringParam(request, "connectorId", 0); + readStringParam(request, "action", 1); + return buildToolIntentResponse(request, "lime.connectors", input, runs); + } + throwUnsupportedMethod(request); +} + +function dispatchUsage( + host: CapabilityHost, + request: AgentAppHostBridgeCapabilityRequest, +): unknown { + const input = readOptionalInputRecord(request); + const tasks = filterRuntimeProjectionTasks(host, request); + const usageItems = tasks + .map(buildUsageProjection) + .filter((item): item is RuntimeUsageProjection => Boolean(item)); + const costItems = tasks + .map(buildCostProjection) + .filter((item): item is RuntimeCostProjection => Boolean(item)); + if (request.method === "getTokenUsage") { + return { + appId: request.appId, + taskId: readString(input.taskId), + window: readString(input.window), + source: "agent_runtime_projection", + taskCount: tasks.length, + totals: aggregateUsage(usageItems), + tasks: usageItems, + }; + } + if (request.method === "getCostSummary") { + return { + appId: request.appId, + taskId: readString(input.taskId), + window: readString(input.window), + source: "agent_runtime_projection", + taskCount: tasks.length, + cost: aggregateCost(costItems), + tasks: costItems, + }; + } + if (request.method === "getBudget") { + return { + appId: request.appId, + scope: readString(input.scope) ?? "app", + status: "not_configured", + reason: "no_agent_runtime_budget_facts", + source: "agent_runtime_projection", + observedCost: aggregateCost(costItems), + }; + } + throwUnsupportedMethod(request); +} + export function createAgentAppCapabilityDispatcher({ host, projection, entryKey, runId, + profile = p0HostCapabilityProfile, + manifestVersion, + agentRuntime, + requirements, + boundary, + integrations, + operations, }: CreateAgentAppCapabilityDispatcherOptions): AgentAppCapabilityDispatcher { return async (request) => { + if (request.capability === "lime.capabilities") { + return dispatchCapabilities(request, profile, { + manifestVersion, + agentRuntime, + requirements, + boundary, + integrations, + operations, + }); + } + assertCapabilityDeclared(projection, request, entryKey); + if (request.capability === "lime.models") { + return dispatchModels(host, request); + } + if (request.capability === "lime.usage") { + return dispatchUsage(host, request); + } + if (request.capability === "lime.skills") { + return dispatchSkills(host, request); + } + if (request.capability === "lime.memory") { + return dispatchMemory(host, request); + } + if (request.capability === "lime.context") { + return dispatchContext(host, request); + } + if (request.capability === "lime.search") { + return dispatchSearch(host, request); + } + if (request.capability === "lime.browser") { + return dispatchBrowser(host, request); + } + if (request.capability === "lime.documents") { + return dispatchDocuments(host, request); + } + if (request.capability === "lime.media") { + return dispatchMedia(host, request); + } + if (request.capability === "lime.mcp") { + return dispatchMcp(host, request); + } + if (request.capability === "lime.terminal") { + return dispatchTerminal(host, request); + } + if (request.capability === "lime.connectors") { + return dispatchConnectors(host, request); + } + const sdk = host.createSdkContext( request.entryKey ?? entryKey, resolveRunId(request, runId), diff --git a/src/features/agent-app/runtime/hostBridge.test.ts b/src/features/agent-app/runtime/hostBridge.test.ts index 89d1ae5d7..5e0547909 100644 --- a/src/features/agent-app/runtime/hostBridge.test.ts +++ b/src/features/agent-app/runtime/hostBridge.test.ts @@ -556,16 +556,24 @@ describe("AgentAppHostBridge", () => { requestId: "req-1", payload: expect.objectContaining({ ok: true, - value: { + value: expect.objectContaining({ taskId: "task-1", traceId: "trace-1", status: "running", - }, - result: { + process: expect.objectContaining({ + terminal: false, + collapsedByDefault: false, + }), + }), + result: expect.objectContaining({ taskId: "task-1", traceId: "trace-1", status: "running", - }, + runtimeProcess: expect.objectContaining({ + terminal: false, + collapsedByDefault: false, + }), + }), }), }), runtimeOrigin, @@ -642,6 +650,13 @@ describe("AgentAppHostBridge", () => { taskId: "task-1", bridgeAction: "contentFactoryProduction", events: [{ eventType: "task:progress", message: "任务正在执行" }], + process: expect.objectContaining({ + terminal: false, + collapsedByDefault: false, + timeline: expect.arrayContaining([ + expect.objectContaining({ message: "任务正在执行" }), + ]), + }), }), }), runtimeOrigin, @@ -734,6 +749,8 @@ describe("AgentAppHostBridge", () => { type: "task:progress", status: "running", message: "后端 runtime event 已到达", + streamKind: "thinking_delta", + delta: "先判断任务边界。", }, }); @@ -752,6 +769,12 @@ describe("AgentAppHostBridge", () => { message: "后端 runtime event 已到达", }), ], + process: expect.objectContaining({ + thinkingText: "先判断任务边界。", + timeline: expect.arrayContaining([ + expect.objectContaining({ title: "思考过程" }), + ]), + }), }), }), runtimeOrigin, @@ -774,6 +797,114 @@ describe("AgentAppHostBridge", () => { } }); + it("Host 已封装 runtimeProcess 时应优先转发 canonical 过程而不是从事件重建", async () => { + vi.useFakeTimers(); + const { frame, postMessage } = buildFrame(); + let runtimeHandler: + | ((event: { payload: unknown }) => void) + | undefined; + const listenRuntimeEventMock = vi.fn( + async ( + _eventName: string, + handler: (event: { payload: unknown }) => void, + ) => { + runtimeHandler = handler; + return vi.fn(); + }, + ); + const listenRuntimeEvent = + listenRuntimeEventMock as unknown as typeof import("@/lib/dev-bridge").safeListen; + const dispatchCapability = vi.fn().mockResolvedValue({ + taskId: "task-1", + taskStatus: "running", + taskEvents: [{ eventType: "task:progress", message: "已有订阅事件" }], + }); + const bridge = new AgentAppHostBridge({ + frame, + appId, + entryKey, + displayName: "内容工厂", + entryRoute: "/dashboard", + entryUrl, + dispatchCapability, + listenRuntimeEvent, + now: () => "2026-05-16T00:00:00.000Z", + }); + const cleanup = bridge.start(); + + try { + window.dispatchEvent( + new MessageEvent("message", { + data: buildAppMessage("capability:subscribe", { + capability: "lime.agent", + topic: "task", + input: { taskId: "task-1", bridgeAction: "contentFactoryProduction" }, + pollIntervalMs: 250, + }), + origin: runtimeOrigin, + source: frame.contentWindow, + }), + ); + await flushBridgeTasks(); + + runtimeHandler?.({ + payload: { + eventType: "task:runtimeEvent", + message: "底层事件只是增量", + runtimeProcess: { + timeline: [ + { + kind: "output", + title: "Host canonical 过程", + statusText: "流式输出", + message: "后端封装全文", + }, + ], + streamText: "后端封装全文", + thinkingText: "后端思考", + executionText: "后端执行", + skillNames: ["article-writer"], + invokedSkillNames: ["article-writer"], + model: { provider: "openai", model: "gpt-4.1", label: "openai/gpt-4.1" }, + usage: { inputTokens: 12, outputTokens: 8, totalTokens: 20 }, + cost: { estimatedTotalCost: 0.0002, currency: "USD" }, + terminal: false, + collapsedByDefault: false, + routingCount: 1, + executionCount: 1, + artifactCount: 0, + }, + }, + }); + + expect(postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: "capability:event", + payload: expect.objectContaining({ + eventType: "task:runtimeEvent", + process: expect.objectContaining({ + streamText: "后端封装全文", + thinkingText: "后端思考", + executionText: "后端执行", + usage: { inputTokens: 12, outputTokens: 8, totalTokens: 20 }, + timeline: [ + expect.objectContaining({ + title: "Host canonical 过程", + message: "后端封装全文", + }), + ], + }), + }), + }), + runtimeOrigin, + ); + } finally { + cleanup(); + vi.useRealTimers(); + } + }); + + it("成功终态未带 artifact 时应继续短轮询直到最终 patch replay", async () => { vi.useFakeTimers(); const { frame, postMessage } = buildFrame(); diff --git a/src/features/agent-app/runtime/hostBridge.ts b/src/features/agent-app/runtime/hostBridge.ts index 05cff8b16..96838bc12 100644 --- a/src/features/agent-app/runtime/hostBridge.ts +++ b/src/features/agent-app/runtime/hostBridge.ts @@ -19,6 +19,8 @@ import { type LimeCapabilityName, } from "../sdk/capabilityContract"; import { toLimeCapabilityError } from "../sdk/capabilityErrors"; +import type { AgentAppRuntimeProcessView } from "../types"; +import { buildAgentRuntimeProcessView } from "./agentRuntimeProcess"; export const AGENT_APP_BRIDGE_PROTOCOL = "lime.agentApp.bridge"; export const AGENT_APP_BRIDGE_VERSION = 1; @@ -75,6 +77,40 @@ export interface AgentAppHostBridgeNotifyPayload { level: "info" | "success" | "warning" | "error"; } +export type AgentAppHostAgentRunUiMode = "drawer" | "modal" | "page"; + +export interface AgentAppHostAgentRunUiRequest { + taskId?: string; + bridgeAction?: string; + title?: string; + mode?: AgentAppHostAgentRunUiMode; + expectedOutput?: unknown; + runtimeProcess?: unknown; + runtimeFacts?: unknown; + task?: unknown; + snapshot?: unknown; + events?: unknown[]; +} + +export interface AgentAppHostAgentRunUiOpenResult { + opened: true; + surface: "host_agent_run"; + mode: AgentAppHostAgentRunUiMode; + taskId?: string; +} + +export interface AgentAppHostAgentRunUiUpdateResult { + updated: true; + surface: "host_agent_run"; + taskId?: string; +} + +export interface AgentAppHostAgentRunUiCloseResult { + closed: true; + surface: "host_agent_run"; + taskId?: string; +} + export interface AgentAppHostBridgeCapabilityRequest { appId: string; entryKey?: string; @@ -100,6 +136,15 @@ export interface CreateAgentAppHostBridgeOptions { locale?: string; notify?: (payload: AgentAppHostBridgeNotifyPayload) => void; openExternal?: (url: string) => void; + openAgentRunUi?: ( + request: AgentAppHostAgentRunUiRequest, + ) => AgentAppHostAgentRunUiOpenResult; + updateAgentRunUi?: ( + request: AgentAppHostAgentRunUiRequest, + ) => AgentAppHostAgentRunUiUpdateResult; + closeAgentRunUi?: ( + request: Pick, + ) => AgentAppHostAgentRunUiCloseResult; capabilities?: AgentAppHostBridgeCapabilities; dispatchCapability?: ( request: AgentAppHostBridgeCapabilityRequest, @@ -249,6 +294,38 @@ function readTaskEventsFromValue(value: unknown): unknown[] { return artifactEvents; } +function taskEventIdentity(event: unknown, index: number): string { + if (!isRecord(event)) { + return `event:${index}`; + } + const stableParts = [ + readString(event, "eventId"), + readString(event, "id"), + readString(event, "type") ?? readString(event, "eventType"), + readString(event, "requestId"), + readString(event, "message"), + readString(event, "occurredAt") ?? readString(event, "at"), + ].filter(Boolean); + return stableParts.length ? stableParts.join(":") : `event:${index}`; +} + +function mergeTaskEvents(...groups: unknown[][]): unknown[] { + const seen = new Set(); + const merged: unknown[] = []; + groups.flat().forEach((event, index) => { + if (!event) { + return; + } + const key = taskEventIdentity(event, index); + if (seen.has(key)) { + return; + } + seen.add(key); + merged.push(event); + }); + return merged; +} + function readArtifactsFromValue(value: unknown): Record[] { if (!isRecord(value)) { return []; @@ -363,6 +440,9 @@ interface AgentAppTaskSubscription { timerId?: number; inFlight: boolean; terminalArtifactReplayPolls: number; + events: unknown[]; + latestTask?: unknown; + process?: AgentAppRuntimeProcessView; } function collectCssVariableTokens(root: HTMLElement): Record { @@ -533,6 +613,15 @@ export class AgentAppHostBridge { private readonly locale?: string; private readonly notify?: (payload: AgentAppHostBridgeNotifyPayload) => void; private readonly openExternal?: (url: string) => void; + private readonly openAgentRunUi?: ( + request: AgentAppHostAgentRunUiRequest, + ) => AgentAppHostAgentRunUiOpenResult; + private readonly updateAgentRunUi?: ( + request: AgentAppHostAgentRunUiRequest, + ) => AgentAppHostAgentRunUiUpdateResult; + private readonly closeAgentRunUi?: ( + request: Pick, + ) => AgentAppHostAgentRunUiCloseResult; private readonly capabilities?: AgentAppHostBridgeCapabilities; private readonly dispatchCapability?: ( request: AgentAppHostBridgeCapabilityRequest, @@ -559,6 +648,9 @@ export class AgentAppHostBridge { this.locale = options.locale; this.notify = options.notify; this.openExternal = options.openExternal; + this.openAgentRunUi = options.openAgentRunUi; + this.updateAgentRunUi = options.updateAgentRunUi; + this.closeAgentRunUi = options.closeAgentRunUi; this.capabilities = options.capabilities; this.dispatchCapability = options.dispatchCapability; this.listenRuntimeEvent = options.listenRuntimeEvent ?? safeListen; @@ -720,12 +812,6 @@ export class AgentAppHostBridge { private async handleCapabilityInvoke( message: LimeAgentAppBridgeMessage, ): Promise> { - if (!this.dispatchCapability) { - throw new AgentAppHostBridgeActionError( - "CAPABILITY_BLOCKED", - "Capability invocation is not enabled for this Agent App runtime.", - ); - } if (!isRecord(message.payload)) { throw new AgentAppHostBridgeActionError( "INVALID_PAYLOAD", @@ -793,13 +879,177 @@ export class AgentAppHostBridge { if (provenance) { request.provenance = provenance; } - const result = await this.dispatchCapability(request); + const uiResult = this.handleUiCapabilityInvoke(request); + if (uiResult) { + return { + ...createLimeCapabilitySuccessResponse(uiResult), + result: uiResult, + }; + } + if (!this.dispatchCapability) { + throw new AgentAppHostBridgeActionError( + "CAPABILITY_BLOCKED", + "Capability invocation is not enabled for this Agent App runtime.", + ); + } + const result = this.enrichAgentCapabilityResult( + request, + await this.dispatchCapability(request), + ); return { ...createLimeCapabilitySuccessResponse(result), result, }; } + private handleUiCapabilityInvoke( + request: AgentAppHostBridgeCapabilityRequest, + ): unknown | null { + if (request.capability !== "lime.ui") { + return null; + } + if (request.method === "toast") { + this.handleToast(request.input); + return { accepted: true }; + } + if (request.method === "navigate") { + const url = this.resolveSameOriginActionUrl(request.input, ["route", "url"]); + window.setTimeout(() => { + if (!this.disposed) { + this.frame.src = url.href; + } + }, 0); + return { navigatedTo: url.pathname + url.search + url.hash }; + } + if (request.method === "openExternal") { + const url = this.resolveExternalUrl(request.input); + (this.openExternal ?? ((target) => window.open(target, "_blank", "noopener,noreferrer")))( + url.href, + ); + return { opened: true }; + } + if (request.method === "download") { + const url = this.resolveSameOriginActionUrl(request.input, ["url", "href"]); + this.downloadSameOriginUrl(url, request.input); + return { downloaded: true }; + } + if (request.method === "getSnapshot") { + return buildAgentAppHostSnapshot({ + appId: this.appId, + entryKey: this.entryKey, + displayName: this.displayName, + entryRoute: this.entryRoute, + entryUrl: this.entryUrl, + locale: this.locale, + now: this.now, + runtimeOrigin: this.runtimeOrigin, + capabilities: this.capabilities, + }) as unknown as Record; + } + if (request.method === "openAgentRun") { + return this.openAgentRunUi?.(this.normalizeAgentRunUiRequest(request.input)) ?? { + opened: true, + surface: "host_agent_run", + mode: "drawer", + taskId: this.readAgentRunUiTaskId(request.input), + }; + } + if (request.method === "updateAgentRun") { + return this.updateAgentRunUi?.(this.normalizeAgentRunUiRequest(request.input)) ?? { + updated: true, + surface: "host_agent_run", + taskId: this.readAgentRunUiTaskId(request.input), + }; + } + if (request.method === "closeAgentRun") { + const normalized = this.normalizeAgentRunUiRequest(request.input); + return this.closeAgentRunUi?.({ + taskId: normalized.taskId, + bridgeAction: normalized.bridgeAction, + }) ?? { + closed: true, + surface: "host_agent_run", + taskId: normalized.taskId, + }; + } + throw new AgentAppHostBridgeActionError( + "UNSUPPORTED_CAPABILITY_METHOD", + `${request.capability}.${request.method} is not supported by Agent App Host Bridge.`, + ); + } + + private normalizeAgentRunUiRequest(input: unknown): AgentAppHostAgentRunUiRequest { + if (!isRecord(input)) { + return {}; + } + const mode = readString(input, "mode"); + return { + taskId: this.readAgentRunUiTaskId(input), + bridgeAction: readString(input, "bridgeAction"), + title: readString(input, "title"), + mode: mode === "modal" || mode === "page" ? mode : "drawer", + expectedOutput: hasOwn(input, "expectedOutput") ? input.expectedOutput : undefined, + runtimeProcess: hasOwn(input, "runtimeProcess") + ? input.runtimeProcess + : hasOwn(input, "process") + ? input.process + : undefined, + runtimeFacts: hasOwn(input, "runtimeFacts") ? input.runtimeFacts : undefined, + task: hasOwn(input, "task") ? input.task : undefined, + snapshot: hasOwn(input, "snapshot") ? input.snapshot : undefined, + events: Array.isArray(input.events) ? input.events : undefined, + }; + } + + private readAgentRunUiTaskId(input: unknown): string | undefined { + if (!isRecord(input)) { + return undefined; + } + return ( + readString(input, "taskId") ?? + (isRecord(input.task) ? readString(input.task, "taskId") : undefined) ?? + (isRecord(input.snapshot) ? readString(input.snapshot, "taskId") : undefined) + ); + } + + private enrichAgentCapabilityResult( + request: AgentAppHostBridgeCapabilityRequest, + result: unknown, + ): unknown { + if (request.capability !== "lime.agent" || !isRecord(result)) { + return result; + } + if ( + isRecord(result.process) && + isRecord(result.runtimeProcess) + ) { + return result; + } + if ( + request.method !== "startTask" && + request.method !== "getTask" && + request.method !== "cancelTask" && + request.method !== "retryTask" + ) { + return result; + } + const process = buildAgentRuntimeProcessView({ + events: readTaskEventsFromValue(result), + task: result, + snapshot: result, + expectedOutput: + isRecord(request.input) && hasOwn(request.input, "expectedOutput") + ? request.input.expectedOutput + : undefined, + lastInput: request.input, + }); + return { + ...result, + runtimeProcess: isRecord(result.runtimeProcess) ? result.runtimeProcess : process, + process: isRecord(result.process) ? result.process : process, + }; + } + private async handleCapabilitySubscribe( message: LimeAgentAppBridgeMessage, ): Promise> { @@ -854,6 +1104,7 @@ export class AgentAppHostBridge { runtimeEventName, inFlight: false, terminalArtifactReplayPolls: 0, + events: [], }); void this.attachRuntimeEventSubscription(subscriptionId); void this.pollTaskSubscription(subscriptionId); @@ -944,6 +1195,11 @@ export class AgentAppHostBridge { return; } const events = readTaskEventsFromValue(payload); + const process = this.updateTaskSubscriptionProcess( + subscription, + payload, + events.length ? events : buildTaskEventsFromRuntimeEventPayload(payload), + ); this.postToApp("capability:event", { subscriptionId, capability: "lime.agent", @@ -954,10 +1210,62 @@ export class AgentAppHostBridge { runtimeEventName: subscription.runtimeEventName, runtimeEvent: payload, events: events.length ? events : buildTaskEventsFromRuntimeEventPayload(payload), + runtimeProcess: process, + process, emittedAt: (this.now ?? (() => new Date().toISOString()))(), }); } + private updateTaskSubscriptionProcess( + subscription: AgentAppTaskSubscription, + value: unknown, + events: unknown[], + ): AgentAppRuntimeProcessView { + if (isRecord(value) && (Array.isArray(value.events) || Array.isArray(value.taskEvents))) { + subscription.latestTask = value; + } + subscription.events = mergeTaskEvents(subscription.events, events); + const explicitProcess = this.readRuntimeProcess(value); + if (explicitProcess) { + subscription.process = explicitProcess; + return explicitProcess; + } + const process = buildAgentRuntimeProcessView({ + events: subscription.events, + task: subscription.latestTask, + snapshot: value, + }); + subscription.process = process; + return process; + } + + private readRuntimeProcess(value: unknown): AgentAppRuntimeProcessView | null { + if (!isRecord(value)) { + return null; + } + if (isRecord(value.runtimeProcess)) { + return value.runtimeProcess as unknown as AgentAppRuntimeProcessView; + } + if (isRecord(value.process)) { + return value.process as unknown as AgentAppRuntimeProcessView; + } + const task = isRecord(value.task) ? value.task : undefined; + if (isRecord(task?.runtimeProcess)) { + return task.runtimeProcess as unknown as AgentAppRuntimeProcessView; + } + if (isRecord(task?.process)) { + return task.process as unknown as AgentAppRuntimeProcessView; + } + const snapshot = isRecord(value.snapshot) ? value.snapshot : undefined; + if (isRecord(snapshot?.runtimeProcess)) { + return snapshot.runtimeProcess as unknown as AgentAppRuntimeProcessView; + } + if (isRecord(snapshot?.process)) { + return snapshot.process as unknown as AgentAppRuntimeProcessView; + } + return null; + } + private async pollTaskSubscription(subscriptionId: string): Promise { const subscription = this.taskSubscriptions.get(subscriptionId); if (!subscription || subscription.inFlight || this.disposed || !this.dispatchCapability) { @@ -968,6 +1276,12 @@ export class AgentAppHostBridge { const result = await this.dispatchCapability( this.buildTaskSubscriptionPollRequest(subscription), ); + const events = readTaskEventsFromValue(result); + const process = this.updateTaskSubscriptionProcess( + subscription, + result, + events, + ); this.postToApp("capability:event", { subscriptionId, capability: "lime.agent", @@ -976,7 +1290,9 @@ export class AgentAppHostBridge { taskId: subscription.taskId, bridgeAction: subscription.bridgeAction, task: result, - events: readTaskEventsFromValue(result), + events, + runtimeProcess: process, + process, emittedAt: (this.now ?? (() => new Date().toISOString()))(), }); const shouldPollForTerminalArtifact = diff --git a/src/features/agent-app/schema/referenceCliCrossCheck.test.ts b/src/features/agent-app/schema/referenceCliCrossCheck.test.ts index 06983fdfb..69b34f9c8 100644 --- a/src/features/agent-app/schema/referenceCliCrossCheck.test.ts +++ b/src/features/agent-app/schema/referenceCliCrossCheck.test.ts @@ -4,10 +4,13 @@ import { existsSync, readFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { describe, expect, it } from "vitest"; import { parse as parseYaml } from "yaml"; -import { buildAgentAppInstallReview, buildLocalAgentAppSourceState } from "../install/installReview"; +import { + buildAgentAppInstallReview, + buildLocalAgentAppSourceState, +} from "../install/installReview"; import { buildInstalledAppPreview } from "../install/installedAppPreview"; import { normalizeManifest } from "../manifest/normalizeManifest"; -import { parseManifest } from "../manifest/parseManifest"; +import { mergeLayeredManifest } from "../manifest/parseManifest"; import { checkReadiness } from "../readiness/checkReadiness"; import { p0HostCapabilityProfile } from "../readiness/hostCapabilityProfile"; import { projectApp } from "../projection/projectApp"; @@ -19,7 +22,8 @@ import type { } from "../types"; const STANDARD_ROOT = resolve( - process.env.LIME_AGENTAPP_STANDARD_ROOT ?? resolve(process.cwd(), "../../limecloud/agentapp"), + process.env.LIME_AGENTAPP_STANDARD_ROOT ?? + resolve(process.cwd(), "../../limecloud/agentapp"), ); const REFERENCE_CLI = join(STANDARD_ROOT, "bin/agentapp-ref.mjs"); const STANDARD_CONTENT_FACTORY_APP = join( @@ -28,6 +32,17 @@ const STANDARD_CONTENT_FACTORY_APP = join( ); const PUBLIC_SCHEMAS = join(STANDARD_ROOT, "docs/public/schemas"); const LOADED_AT = "2026-05-15T00:00:00.000Z"; +const LAYERED_MANIFEST_FILES = [ + "app.capabilities.yaml", + "app.entries.yaml", + "app.permissions.yaml", + "app.errors.yaml", + "app.i18n.yaml", + "app.signature.yaml", + "app.runtime.yaml", + "evals/readiness.yaml", + "evals/health.yaml", +] as const; const SETUP_KINDS = new Set([ "skill", @@ -60,7 +75,8 @@ interface ReferenceProjection { manifestVersion: string; }; capabilityRequirements: { - capabilities: Record; + sdk?: string; + capabilities: Record | string[]; }; entries: ReferenceProjectionItem[]; storage?: { @@ -116,23 +132,39 @@ const describeIfReferenceAvailable = function runReferenceCli(command: "validate" | "project" | "readiness"): T { return JSON.parse( - execFileSync("node", [REFERENCE_CLI, command, STANDARD_CONTENT_FACTORY_APP], { - encoding: "utf8", - }), + execFileSync( + "node", + [REFERENCE_CLI, command, STANDARD_CONTENT_FACTORY_APP], + { + encoding: "utf8", + }, + ), ) as T; } function readPublicSchema(name: string): PublicJsonSchema { - return JSON.parse(readFileSync(join(PUBLIC_SCHEMAS, name), "utf8")) as PublicJsonSchema; + return JSON.parse( + readFileSync(join(PUBLIC_SCHEMAS, name), "utf8"), + ) as PublicJsonSchema; } function readStandardManifest(): AppManifest { - const appMarkdown = readFileSync(join(STANDARD_CONTENT_FACTORY_APP, "APP.md"), "utf8"); + const appMarkdown = readFileSync( + join(STANDARD_CONTENT_FACTORY_APP, "APP.md"), + "utf8", + ); const frontmatter = appMarkdown.match(/^---\n([\s\S]*?)\n---/); if (!frontmatter) { - throw new Error("Standard content-factory-app APP.md is missing YAML frontmatter."); + throw new Error( + "Standard content-factory-app APP.md is missing YAML frontmatter.", + ); } - return parseManifest(parseYaml(frontmatter[1])); + const layers = LAYERED_MANIFEST_FILES.map((relativePath) => + join(STANDARD_CONTENT_FACTORY_APP, relativePath), + ) + .filter(existsSync) + .map((path) => parseYaml(readFileSync(path, "utf8"))); + return mergeLayeredManifest(parseYaml(frontmatter[1]), layers); } function buildReferenceAlignedProfile( @@ -142,15 +174,14 @@ function buildReferenceAlignedProfile( ...p0HostCapabilityProfile.capabilities, }; - Object.keys(referenceProjection.capabilityRequirements.capabilities).forEach( - (capability) => { - capabilities[capability] = { - version: "0.3.0", - enabled: true, - implementation: "mock", - }; - }, - ); + referenceCapabilityKeys(referenceProjection).forEach((capability) => { + capabilities[capability] = { + version: + p0HostCapabilityProfile.capabilities[capability]?.version ?? "0.3.0", + enabled: true, + implementation: "mock", + }; + }); return { ...p0HostCapabilityProfile, @@ -206,11 +237,26 @@ function keys(items: ReferenceProjectionItem[]): string[] { return items.map((item) => String(item.key ?? item.id)).sort(); } -function setupCheckKey(item: { kind?: string; key?: string; required?: boolean }): string { +function referenceCapabilityKeys( + referenceProjection: ReferenceProjection, +): string[] { + const capabilities = referenceProjection.capabilityRequirements.capabilities; + return Array.isArray(capabilities) + ? [...capabilities].sort() + : Object.keys(capabilities).sort(); +} + +function setupCheckKey(item: { + kind?: string; + key?: string; + required?: boolean; +}): string { return `${item.kind}:${item.key}:${item.required === true}`; } -function toReferenceReadinessStatus(status: ReadinessStatus): ReferenceReadiness["status"] { +function toReferenceReadinessStatus( + status: ReadinessStatus, +): ReferenceReadiness["status"] { if (status === "ready") { return "ready"; } @@ -220,145 +266,210 @@ function toReferenceReadinessStatus(status: ReadinessStatus): ReferenceReadiness return "needs-setup"; } -describeIfReferenceAvailable("Agent App P17.2.5 standard reference cross-check", () => { - it("应能运行上游 reference CLI,并读取 public schema 作为字段事实源", () => { - const validation = runReferenceCli<{ - ok: boolean; - status: string; - manifestHash: string; - findings: unknown[]; - }>("validate"); - const projectionSchema = readPublicSchema("app-projection.schema.json"); - const readinessSchema = readPublicSchema("app-readiness.schema.json"); +describeIfReferenceAvailable( + "Agent App P17.2.5 standard reference cross-check", + () => { + it("应能运行上游 reference CLI,并读取 public schema 作为字段事实源", () => { + const validation = runReferenceCli<{ + ok: boolean; + status: string; + manifestHash: string; + findings: unknown[]; + }>("validate"); + const projectionSchema = readPublicSchema("app-projection.schema.json"); + const readinessSchema = readPublicSchema("app-readiness.schema.json"); - expect(validation).toMatchObject({ - ok: true, - status: "passed", - findings: [], + expect(validation).toMatchObject({ + ok: true, + status: "passed", + findings: [], + }); + expect(validation.manifestHash).toMatch(/^sha256:[a-f0-9]{64}$/); + expect(projectionSchema.required).toEqual( + expect.arrayContaining(["app", "entries", "provenance"]), + ); + expect(readinessSchema.required).toEqual( + expect.arrayContaining(["ok", "status", "checks"]), + ); }); - expect(validation.manifestHash).toMatch(/^sha256:[a-f0-9]{64}$/); - expect(projectionSchema.required).toEqual( - expect.arrayContaining(["app", "entries", "provenance"]), - ); - expect(readinessSchema.required).toEqual( - expect.arrayContaining(["ok", "status", "checks"]), - ); - }); - it("应把上游 projection schema 字段映射到 Lime projection,缺口必须显式登记", () => { - const projectionSchema = readPublicSchema("app-projection.schema.json"); - const referenceProjection = runReferenceCli("project"); - const { projection } = buildClientArtifacts(referenceProjection); - const limeProjectionFieldMap: Record = { - app: "app", - capabilityRequirements: "requiredCapabilities", - entries: "entries", - ui: "ui", - storage: "storage", - services: "services", - workflows: "workflows", - knowledgeTemplates: "knowledgeBindings", - toolRequirements: "toolRequirements", - artifactTypes: "artifactTypes", - evals: "evals", - events: "events", - secrets: "secrets", - overlayTemplates: "overlayTemplates", - lifecycle: "lifecycle", - provenance: "provenance", - }; - const acceptedDivergences: Record = { - ok: "reference CLI envelope field; Lime projection is not a CLI response.", - command: "reference CLI envelope field; Lime projection is not a CLI response.", - permissions: - "P17.2 exposes permissions through install review permissionCount; full permission projection is deferred to P17.3/P18.", - }; - const schemaFields = Object.keys(projectionSchema.properties ?? {}); - const unmappedFields = schemaFields.filter( - (field) => !limeProjectionFieldMap[field] && !acceptedDivergences[field], - ); + it("应把上游 projection schema 字段映射到 Lime projection,缺口必须显式登记", () => { + const projectionSchema = readPublicSchema("app-projection.schema.json"); + const referenceProjection = + runReferenceCli("project"); + const { projection } = buildClientArtifacts(referenceProjection); + const limeProjectionFieldMap: Record = { + app: "app", + capabilityRequirements: "requiredCapabilities", + entries: "entries", + ui: "ui", + storage: "storage", + services: "services", + workflows: "workflows", + knowledgeTemplates: "knowledgeBindings", + toolRequirements: "toolRequirements", + artifactTypes: "artifactTypes", + evals: "evals", + events: "events", + secrets: "secrets", + overlayTemplates: "overlayTemplates", + lifecycle: "lifecycle", + provenance: "provenance", + }; + const acceptedDivergences: Record = { + ok: "reference CLI envelope field; Lime projection is not a CLI response.", + command: + "reference CLI envelope field; Lime projection is not a CLI response.", + permissions: + "P17.2 exposes permissions through install review permissionCount; full permission projection is deferred to P17.3/P18.", + triggers: + "Agent App v0.5/v0.6 discovery metadata; Lime projection keeps runtime readiness first and will project discovery metadata in P18.7-C/F.", + quickstart: + "Agent App v0.5/v0.6 onboarding metadata; Lime setup guidance is still derived from readiness until P18.7-C/F.", + skills: + "Agent App v0.5/v0.6 skill package metadata; runtime skill binding projection is deferred to P18.7-D.", + publisher: + "Agent App v0.5/v0.6 marketplace metadata; install review currently keeps package identity only.", + author: + "Agent App v0.5/v0.6 marketplace metadata; install review currently keeps package identity only.", + maintainers: + "Agent App v0.5/v0.6 marketplace metadata; install review currently keeps package identity only.", + contributors: + "Agent App v0.5/v0.6 marketplace metadata; install review currently keeps package identity only.", + timeline: + "Agent App v0.5/v0.6 release metadata; package lifecycle projection is deferred until cloud release review.", + supportWindow: + "Agent App v0.5/v0.6 support metadata; host policy projection is deferred until marketplace install review.", + links: + "Agent App v0.5/v0.6 external links; current Lime projection avoids exposing external metadata before policy review.", + license: + "Agent App v0.5/v0.6 license metadata; install review currently keeps package identity only.", + support: + "Agent App v0.5/v0.6 support metadata; install review currently keeps package identity only.", + distribution: + "Agent App v0.5/v0.6 distribution metadata; cloud catalog projection owns this later.", + compliance: + "Agent App v0.5/v0.6 compliance metadata; enterprise policy projection owns this later.", + errors: + "reference CLI diagnostics field; Lime projection reports errors through validation/readiness surfaces.", + i18n: "Agent App v0.5/v0.6 localization metadata; Lime projection does not yet expose localized marketplace copy.", + signature: + "Agent App v0.5/v0.6 signature metadata; package verification owns signature state later.", + health: + "Agent App v0.5/v0.6 health metadata; runtime health projection is deferred to Host discovery.", + agentRuntime: + "Agent App v0.6 task runtime control plane is preserved on normalized manifest; projection into runtime policy is deferred to Agent App runtime execution.", + requirements: + "Agent App v0.7 requirement boundary is preserved on normalized manifest; projection into install/readiness UX is deferred to capability handoff review.", + boundary: + "Agent App v0.7 responsibility boundary is preserved on normalized manifest; projection into Host/Cloud setup UX is deferred to capability handoff review.", + integrations: + "Agent App v0.7 external integration requirements are preserved on normalized manifest; connector readiness projection is deferred to Host/Cloud setup review.", + operations: + "Agent App v0.7 operation side-effect policy is preserved on normalized manifest; execution gating remains Host policy responsibility.", + }; + const schemaFields = Object.keys(projectionSchema.properties ?? {}); + const unmappedFields = schemaFields.filter( + (field) => + !limeProjectionFieldMap[field] && !acceptedDivergences[field], + ); - expect(unmappedFields).toEqual([]); - expect(projection.app).toMatchObject({ - appId: referenceProjection.app.name, - version: referenceProjection.app.version, - status: referenceProjection.app.status, - appType: referenceProjection.app.appType, - description: referenceProjection.app.description, + expect(unmappedFields).toEqual([]); + expect(projection.app).toMatchObject({ + appId: referenceProjection.app.name, + version: referenceProjection.app.version, + status: referenceProjection.app.status, + appType: referenceProjection.app.appType, + description: referenceProjection.app.description, + }); + expect(projection.provenance).toMatchObject({ + appId: referenceProjection.provenance.appName, + appVersion: referenceProjection.provenance.appVersion, + packageHash: referenceProjection.provenance.packageHash, + manifestHash: referenceProjection.provenance.manifestHash, + }); + expect(projection.entries.map((entry) => entry.key).sort()).toEqual( + keys(referenceProjection.entries), + ); + expect( + projection.requiredCapabilities.map((item) => item.capability).sort(), + ).toEqual(referenceCapabilityKeys(referenceProjection)); + expect( + projection.knowledgeBindings.map((item) => item.key).sort(), + ).toEqual(keys(referenceProjection.knowledgeTemplates)); + expect( + projection.toolRequirements.map((item) => item.key).sort(), + ).toEqual(keys(referenceProjection.toolRequirements)); + expect(projection.artifactTypes.map((item) => item.key).sort()).toEqual( + keys(referenceProjection.artifactTypes), + ); + expect(projection.evals.map((item) => item.key).sort()).toEqual( + keys(referenceProjection.evals), + ); + expect(projection.services.map((item) => item.key).sort()).toEqual( + keys(referenceProjection.services), + ); + expect(projection.workflows.map((item) => item.key).sort()).toEqual( + keys(referenceProjection.workflows), + ); + expect(projection.secrets.map((item) => item.key).sort()).toEqual( + keys(referenceProjection.secrets), + ); + expect( + projection.overlayTemplates.map((item) => item.key).sort(), + ).toEqual(keys(referenceProjection.overlayTemplates)); + expect(projection.storage?.namespace).toBe( + referenceProjection.storage?.namespace, + ); }); - expect(projection.provenance).toMatchObject({ - appId: referenceProjection.provenance.appName, - appVersion: referenceProjection.provenance.appVersion, - packageHash: referenceProjection.provenance.packageHash, - manifestHash: referenceProjection.provenance.manifestHash, - }); - expect(projection.entries.map((entry) => entry.key).sort()).toEqual( - keys(referenceProjection.entries), - ); - expect(projection.requiredCapabilities.map((item) => item.capability).sort()).toEqual( - Object.keys(referenceProjection.capabilityRequirements.capabilities).sort(), - ); - expect(projection.knowledgeBindings.map((item) => item.key).sort()).toEqual( - keys(referenceProjection.knowledgeTemplates), - ); - expect(projection.toolRequirements.map((item) => item.key).sort()).toEqual( - keys(referenceProjection.toolRequirements), - ); - expect(projection.artifactTypes.map((item) => item.key).sort()).toEqual( - keys(referenceProjection.artifactTypes), - ); - expect(projection.evals.map((item) => item.key).sort()).toEqual( - keys(referenceProjection.evals), - ); - expect(projection.services.map((item) => item.key).sort()).toEqual( - keys(referenceProjection.services), - ); - expect(projection.workflows.map((item) => item.key).sort()).toEqual( - keys(referenceProjection.workflows), - ); - expect(projection.secrets.map((item) => item.key).sort()).toEqual( - keys(referenceProjection.secrets), - ); - expect(projection.overlayTemplates.map((item) => item.key).sort()).toEqual( - keys(referenceProjection.overlayTemplates), - ); - expect(projection.storage?.namespace).toBe(referenceProjection.storage?.namespace); - }); - it("应把 reference readiness checks 映射到 Lime readiness 和 install review descriptor", () => { - const referenceProjection = runReferenceCli("project"); - const referenceReadiness = runReferenceCli("readiness"); - const { readiness, review } = buildClientArtifacts(referenceProjection); - const referenceSetupChecks = referenceReadiness.checks - .filter((check) => SETUP_KINDS.has(check.kind)) - .map(setupCheckKey) - .sort(); - const limeSetupChecks = readiness.warnings - .filter((issue) => issue.kind && SETUP_KINDS.has(issue.kind)) - .map(setupCheckKey) - .sort(); + it("应把 reference readiness checks 映射到 Lime readiness 和 install review descriptor", () => { + const referenceProjection = + runReferenceCli("project"); + const referenceReadiness = + runReferenceCli("readiness"); + const { normalized, readiness, review } = + buildClientArtifacts(referenceProjection); + const referenceSetupChecks = referenceReadiness.checks + .filter((check) => SETUP_KINDS.has(check.kind)) + .map(setupCheckKey) + .sort(); + const limeSetupChecks = readiness.warnings + .filter((issue) => issue.kind && SETUP_KINDS.has(issue.kind)) + .map(setupCheckKey) + .sort(); - expect(toReferenceReadinessStatus(readiness.status)).toBe(referenceReadiness.status); - expect(limeSetupChecks).toEqual(referenceSetupChecks); - expect(readiness.supportedCapabilities.map((item) => item.capability).sort()).toEqual( - referenceReadiness.checks + expect(toReferenceReadinessStatus(readiness.status)).toBe( + referenceReadiness.status, + ); + expect(limeSetupChecks).toEqual(referenceSetupChecks); + const referenceCapabilityChecks = referenceReadiness.checks .filter((check) => check.kind === "capability") .map((check) => check.key) - .sort(), - ); - expect(review).toMatchObject({ - appId: referenceProjection.app.name, - version: referenceProjection.app.version, - manifestVersion: "0.3", - packageHash: referenceProjection.provenance.packageHash, - manifestHash: referenceProjection.provenance.manifestHash, - entryCount: referenceProjection.entries.length, - permissionCount: referenceProjection.permissions.length, - storageNamespace: referenceProjection.storage?.namespace, - readinessStatus: readiness.status, + .sort(); + expect( + readiness.supportedCapabilities.map((item) => item.capability).sort(), + ).toEqual( + referenceCapabilityChecks.length > 0 + ? referenceCapabilityChecks + : referenceCapabilityKeys(referenceProjection), + ); + expect(review).toMatchObject({ + appId: referenceProjection.app.name, + version: referenceProjection.app.version, + manifestVersion: normalized.manifestVersion, + packageHash: referenceProjection.provenance.packageHash, + manifestHash: referenceProjection.provenance.manifestHash, + entryCount: referenceProjection.entries.length, + storageNamespace: referenceProjection.storage?.namespace, + readinessStatus: readiness.status, + }); + expect(review.permissionCount).toBeGreaterThanOrEqual( + referenceProjection.permissions.length, + ); + expect(review.requiredCapabilityKeys.sort()).toEqual( + referenceCapabilityKeys(referenceProjection), + ); }); - expect(review.requiredCapabilityKeys.sort()).toEqual( - Object.keys(referenceProjection.capabilityRequirements.capabilities).sort(), - ); - }); -}); + }, +); diff --git a/src/features/agent-app/sdk/capabilityAdapters.test.ts b/src/features/agent-app/sdk/capabilityAdapters.test.ts index 47dda721c..71bd61c29 100644 --- a/src/features/agent-app/sdk/capabilityAdapters.test.ts +++ b/src/features/agent-app/sdk/capabilityAdapters.test.ts @@ -18,13 +18,19 @@ import { createLimeCoreCapabilityAdapters, LimeCapabilityAdapterError, } from "./capabilityAdapters"; +import { + LIME_CAPABILITY_DEFINITIONS, + getLimeCapabilityAdapterKey, +} from "./capabilityCatalog"; const recordProvenance = { sourceKind: "agent_app", appId: "content-factory-app", appVersion: "1.0.0", - packageHash: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - manifestHash: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + packageHash: + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + manifestHash: + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", entryKey: "dashboard", workflowRunId: "run-1", workspaceId: "workspace-1", @@ -169,6 +175,32 @@ const agentStreamEvents: AgentAppTaskStreamEvent[] = [ ]; describe("P18.3 / P18.4 core capability adapters", () => { + it("应按 catalog 生成所有 lime.* typed adapter,不再只覆盖局部能力", () => { + const adapters = createLimeCoreCapabilityAdapters({ + invoker: createLimeCapabilityInvoker(createMockLimeCapabilityTransport()), + provenance, + }); + + expect(Object.keys(adapters).sort()).toEqual( + LIME_CAPABILITY_DEFINITIONS.map((definition) => + getLimeCapabilityAdapterKey(definition.name), + ).sort(), + ); + LIME_CAPABILITY_DEFINITIONS.forEach((definition) => { + const adapter = adapters[ + getLimeCapabilityAdapterKey(definition.name) + ] as Record; + definition.methods.forEach((method) => { + expect(adapter[method]).toEqual(expect.any(Function)); + }); + }); + expect(adapters.storage.namespace).toBe(provenance.appId); + expect(adapters.models.estimateCost).toEqual(expect.any(Function)); + expect(adapters.skills.list).toEqual(expect.any(Function)); + expect(adapters.policy.check).toEqual(expect.any(Function)); + expect(adapters.usage.getTokenUsage).toEqual(expect.any(Function)); + }); + it("应把 ui / storage / artifacts / evidence / knowledge / tools 调用转成 typed request 并附加 provenance", async () => { const requests: LimeCapabilityInvokeRequest[] = []; const invoker = createLimeCapabilityInvoker( @@ -178,6 +210,15 @@ describe("P18.3 / P18.4 core capability adapters", () => { requests.push(request); return { accepted: true }; }, + openAgentRun: (request) => { + requests.push(request); + return { + opened: true, + surface: "host_agent_run", + mode: "drawer", + taskId: (request.args as { taskId?: string }).taskId, + }; + }, }, "lime.storage": { set: (request) => { @@ -261,6 +302,14 @@ describe("P18.3 / P18.4 core capability adapters", () => { { message: "已保存", level: "success" }, { requestId: "req-ui" }, ); + const agentRunUi = await adapters.ui.openAgentRun( + { + taskId: "agent-task-1", + title: "生成内容批次", + mode: "drawer", + }, + { requestId: "req-ui-agent-run" }, + ); const stored = await adapters.storage.set( { key: "drafts/scenario", value: { title: "内容场景草稿" } }, { @@ -294,6 +343,12 @@ describe("P18.3 / P18.4 core capability adapters", () => { ); expect(adapters.storage.namespace).toBe("content_factory_app"); + expect(agentRunUi).toEqual({ + opened: true, + surface: "host_agent_run", + mode: "drawer", + taskId: "agent-task-1", + }); expect(stored).toMatchObject({ key: "drafts/scenario", value: { title: "内容场景草稿" }, @@ -312,6 +367,12 @@ describe("P18.3 / P18.4 core capability adapters", () => { requestId: "req-ui", provenance, }), + expect.objectContaining({ + capability: "lime.ui", + method: "openAgentRun", + requestId: "req-ui-agent-run", + provenance, + }), expect.objectContaining({ capability: "lime.storage", method: "set", diff --git a/src/features/agent-app/sdk/capabilityAdapters.ts b/src/features/agent-app/sdk/capabilityAdapters.ts index 10bf67fd2..2d70077f1 100644 --- a/src/features/agent-app/sdk/capabilityAdapters.ts +++ b/src/features/agent-app/sdk/capabilityAdapters.ts @@ -7,6 +7,11 @@ import { type LimeCapabilityName, type LimeCapabilityValue, } from "./capabilityContract"; +import { + LIME_CAPABILITY_DEFINITIONS, + getLimeCapabilityAdapterKey, + type LimeCapabilityDefinitionRecord, +} from "./capabilityCatalog"; import type { LimeCapabilityError } from "./capabilityErrors"; export interface LimeCapabilityAdapterCallOptions { @@ -50,6 +55,14 @@ type CapabilityAdapterMethod< options?: LimeCapabilityAdapterCallOptions, ) => Promise>; +type OptionalArgsCapabilityAdapterMethod< + Capability extends LimeCapabilityName, + Method extends LimeCapabilityMethod, +> = ( + args?: LimeCapabilityArgs, + options?: LimeCapabilityAdapterCallOptions, +) => Promise>; + type NoArgsCapabilityAdapterMethod< Capability extends LimeCapabilityName, Method extends LimeCapabilityMethod, @@ -57,74 +70,91 @@ type NoArgsCapabilityAdapterMethod< options?: LimeCapabilityAdapterCallOptions, ) => Promise>; -export interface LimeUiCapabilityAdapter { - readonly toast: CapabilityAdapterMethod<"lime.ui", "toast">; - readonly navigate: CapabilityAdapterMethod<"lime.ui", "navigate">; - readonly openExternal: CapabilityAdapterMethod<"lime.ui", "openExternal">; - readonly download: CapabilityAdapterMethod<"lime.ui", "download">; - readonly getSnapshot: NoArgsCapabilityAdapterMethod< - "lime.ui", - "getSnapshot" +type CapabilityAdapterMethodFor< + Capability extends LimeCapabilityName, + Method extends LimeCapabilityMethod, +> = [LimeCapabilityArgs] extends [undefined] + ? NoArgsCapabilityAdapterMethod + : undefined extends LimeCapabilityArgs + ? OptionalArgsCapabilityAdapterMethod + : CapabilityAdapterMethod; + +export type LimeCapabilityAdapter = { + readonly [Method in LimeCapabilityMethod]: CapabilityAdapterMethodFor< + Capability, + Method >; -} +}; -export interface LimeStorageCapabilityAdapter { - readonly namespace: string; - readonly get: CapabilityAdapterMethod<"lime.storage", "get">; - readonly set: CapabilityAdapterMethod<"lime.storage", "set">; - readonly list: NoArgsCapabilityAdapterMethod<"lime.storage", "list">; - readonly delete: CapabilityAdapterMethod<"lime.storage", "delete">; -} +type LimeCapabilityAdapterKey = + Capability extends `lime.${infer Key}` ? Key : never; -export interface LimeAgentCapabilityAdapter { - readonly startTask: CapabilityAdapterMethod<"lime.agent", "startTask">; - readonly streamTask: CapabilityAdapterMethod<"lime.agent", "streamTask">; - readonly getTask: CapabilityAdapterMethod<"lime.agent", "getTask">; - readonly cancelTask: CapabilityAdapterMethod<"lime.agent", "cancelTask">; - readonly retryTask: CapabilityAdapterMethod<"lime.agent", "retryTask">; - readonly submitHostResponse: CapabilityAdapterMethod< - "lime.agent", - "submitHostResponse" - >; - readonly listTasks: NoArgsCapabilityAdapterMethod< - "lime.agent", - "listTasks" - >; -} +type LimeCapabilityAdapterFor = + Capability extends "lime.storage" + ? LimeCapabilityAdapter & { readonly namespace: string } + : LimeCapabilityAdapter; -export interface LimeArtifactsCapabilityAdapter { - readonly create: CapabilityAdapterMethod<"lime.artifacts", "create">; - readonly open: CapabilityAdapterMethod<"lime.artifacts", "open">; - readonly export: CapabilityAdapterMethod<"lime.artifacts", "export">; -} +export type LimeCoreCapabilityAdapters = { + readonly [Capability in LimeCapabilityName as LimeCapabilityAdapterKey]: LimeCapabilityAdapterFor; +}; -export interface LimeEvidenceCapabilityAdapter { - readonly record: CapabilityAdapterMethod<"lime.evidence", "record">; - readonly linkArtifact: CapabilityAdapterMethod< - "lime.evidence", - "linkArtifact" - >; -} +export type LimeUiCapabilityAdapter = LimeCapabilityAdapter<"lime.ui">; +export type LimeStorageCapabilityAdapter = + LimeCapabilityAdapterFor<"lime.storage">; +export type LimeFilesCapabilityAdapter = LimeCapabilityAdapter<"lime.files">; +export type LimeAgentCapabilityAdapter = LimeCapabilityAdapter<"lime.agent">; +export type LimeKnowledgeCapabilityAdapter = + LimeCapabilityAdapter<"lime.knowledge">; +export type LimeToolsCapabilityAdapter = LimeCapabilityAdapter<"lime.tools">; +export type LimeArtifactsCapabilityAdapter = + LimeCapabilityAdapter<"lime.artifacts">; +export type LimeWorkflowCapabilityAdapter = + LimeCapabilityAdapter<"lime.workflow">; +export type LimePolicyCapabilityAdapter = LimeCapabilityAdapter<"lime.policy">; +export type LimeSecretsCapabilityAdapter = + LimeCapabilityAdapter<"lime.secrets">; +export type LimeEvidenceCapabilityAdapter = + LimeCapabilityAdapter<"lime.evidence">; +export type LimeEventsCapabilityAdapter = LimeCapabilityAdapter<"lime.events">; +export type LimeCapabilitiesCapabilityAdapter = + LimeCapabilityAdapter<"lime.capabilities">; +export type LimeModelsCapabilityAdapter = LimeCapabilityAdapter<"lime.models">; +export type LimeUsageCapabilityAdapter = LimeCapabilityAdapter<"lime.usage">; +export type LimeMemoryCapabilityAdapter = LimeCapabilityAdapter<"lime.memory">; +export type LimeSkillsCapabilityAdapter = LimeCapabilityAdapter<"lime.skills">; +export type LimeMcpCapabilityAdapter = LimeCapabilityAdapter<"lime.mcp">; +export type LimeBrowserCapabilityAdapter = + LimeCapabilityAdapter<"lime.browser">; +export type LimeSearchCapabilityAdapter = LimeCapabilityAdapter<"lime.search">; +export type LimeDocumentsCapabilityAdapter = + LimeCapabilityAdapter<"lime.documents">; +export type LimeMediaCapabilityAdapter = LimeCapabilityAdapter<"lime.media">; +export type LimeTerminalCapabilityAdapter = + LimeCapabilityAdapter<"lime.terminal">; +export type LimeTasksCapabilityAdapter = LimeCapabilityAdapter<"lime.tasks">; +export type LimeSettingsCapabilityAdapter = + LimeCapabilityAdapter<"lime.settings">; +export type LimeWorkspaceCapabilityAdapter = + LimeCapabilityAdapter<"lime.workspace">; +export type LimeContextCapabilityAdapter = + LimeCapabilityAdapter<"lime.context">; +export type LimeConnectorsCapabilityAdapter = + LimeCapabilityAdapter<"lime.connectors">; +export type LimeAutomationCapabilityAdapter = + LimeCapabilityAdapter<"lime.automation">; +export type LimeReviewCapabilityAdapter = LimeCapabilityAdapter<"lime.review">; -export interface LimeKnowledgeCapabilityAdapter { - readonly search: CapabilityAdapterMethod<"lime.knowledge", "search">; - readonly bindStatus: CapabilityAdapterMethod<"lime.knowledge", "bindStatus">; -} - -export interface LimeToolsCapabilityAdapter { - readonly invoke: CapabilityAdapterMethod<"lime.tools", "invoke">; - readonly getProgress: CapabilityAdapterMethod<"lime.tools", "getProgress">; -} - -export interface LimeCoreCapabilityAdapters { - readonly ui: LimeUiCapabilityAdapter; - readonly storage: LimeStorageCapabilityAdapter; - readonly agent: LimeAgentCapabilityAdapter; - readonly artifacts: LimeArtifactsCapabilityAdapter; - readonly evidence: LimeEvidenceCapabilityAdapter; - readonly knowledge: LimeKnowledgeCapabilityAdapter; - readonly tools: LimeToolsCapabilityAdapter; -} +const NO_ARGS_CAPABILITY_METHOD_KEYS = new Set([ + "lime.ui.getSnapshot", + "lime.storage.list", + "lime.agent.listTasks", + "lime.events.listSubscriptions", + "lime.capabilities.list", + "lime.capabilities.getProfile", + "lime.mcp.listServers", + "lime.workspace.getCurrent", + "lime.workspace.list", +]); async function callCapability< Capability extends LimeCapabilityName, @@ -154,93 +184,63 @@ async function callCapability< throw new LimeCapabilityAdapterError(response.error); } +type BoundCapabilityCall = < + Capability extends LimeCapabilityName, + Method extends LimeCapabilityMethod, +>( + capability: Capability, + method: Method, + args: LimeCapabilityArgs | undefined, + callOptions?: LimeCapabilityAdapterCallOptions, +) => Promise>; + +function createCapabilityAdapter( + definition: LimeCapabilityDefinitionRecord & { name: Capability }, + call: BoundCapabilityCall, +): LimeCapabilityAdapter { + const adapter: Record = {}; + definition.methods.forEach((methodName) => { + const method = methodName as LimeCapabilityMethod; + const methodKey = `${definition.name}.${methodName}`; + adapter[methodName] = (argsOrOptions?: unknown, maybeOptions?: unknown) => { + if (NO_ARGS_CAPABILITY_METHOD_KEYS.has(methodKey)) { + return call( + definition.name, + method, + undefined, + argsOrOptions as LimeCapabilityAdapterCallOptions | undefined, + ); + } + return call( + definition.name, + method, + argsOrOptions as LimeCapabilityArgs, + maybeOptions as LimeCapabilityAdapterCallOptions | undefined, + ); + }; + }); + return adapter as LimeCapabilityAdapter; +} + export function createLimeCoreCapabilityAdapters( options: CreateLimeCoreCapabilityAdaptersOptions, ): LimeCoreCapabilityAdapters { const { invoker, provenance } = options; - const call = < - Capability extends LimeCapabilityName, - Method extends LimeCapabilityMethod, - >( - capability: Capability, - method: Method, - args: LimeCapabilityArgs | undefined, - callOptions?: LimeCapabilityAdapterCallOptions, - ) => - callCapability( - invoker, - provenance, - capability, - method, - args, - callOptions, - ); + const call: BoundCapabilityCall = (capability, method, args, callOptions) => + callCapability(invoker, provenance, capability, method, args, callOptions); + const adapters: Record = {}; - return { - ui: { - toast: (args, callOptions) => - call("lime.ui", "toast", args, callOptions), - navigate: (args, callOptions) => - call("lime.ui", "navigate", args, callOptions), - openExternal: (args, callOptions) => - call("lime.ui", "openExternal", args, callOptions), - download: (args, callOptions) => - call("lime.ui", "download", args, callOptions), - getSnapshot: (callOptions) => - call("lime.ui", "getSnapshot", undefined, callOptions), - }, - storage: { - namespace: options.storageNamespace ?? provenance?.appId ?? "agent_app", - get: (args, callOptions) => - call("lime.storage", "get", args, callOptions), - set: (args, callOptions) => - call("lime.storage", "set", args, callOptions), - list: (callOptions) => - call("lime.storage", "list", undefined, callOptions), - delete: (args, callOptions) => - call("lime.storage", "delete", args, callOptions), - }, - agent: { - startTask: (args, callOptions) => - call("lime.agent", "startTask", args, callOptions), - streamTask: (args, callOptions) => - call("lime.agent", "streamTask", args, callOptions), - getTask: (args, callOptions) => - call("lime.agent", "getTask", args, callOptions), - cancelTask: (args, callOptions) => - call("lime.agent", "cancelTask", args, callOptions), - retryTask: (args, callOptions) => - call("lime.agent", "retryTask", args, callOptions), - submitHostResponse: (args, callOptions) => - call("lime.agent", "submitHostResponse", args, callOptions), - listTasks: (callOptions) => - call("lime.agent", "listTasks", undefined, callOptions), - }, - artifacts: { - create: (args, callOptions) => - call("lime.artifacts", "create", args, callOptions), - open: (args, callOptions) => - call("lime.artifacts", "open", args, callOptions), - export: (args, callOptions) => - call("lime.artifacts", "export", args, callOptions), - }, - evidence: { - record: (args, callOptions) => - call("lime.evidence", "record", args, callOptions), - linkArtifact: (args, callOptions) => - call("lime.evidence", "linkArtifact", args, callOptions), - }, - knowledge: { - search: (args, callOptions) => - call("lime.knowledge", "search", args, callOptions), - bindStatus: (args, callOptions) => - call("lime.knowledge", "bindStatus", args, callOptions), - }, - tools: { - invoke: (args, callOptions) => - call("lime.tools", "invoke", args, callOptions), - getProgress: (args, callOptions) => - call("lime.tools", "getProgress", args, callOptions), - }, - }; + LIME_CAPABILITY_DEFINITIONS.forEach((definition) => { + const adapter = createCapabilityAdapter(definition, call); + adapters[getLimeCapabilityAdapterKey(definition.name)] = + definition.name === "lime.storage" + ? { + namespace: + options.storageNamespace ?? provenance?.appId ?? "agent_app", + ...adapter, + } + : adapter; + }); + + return adapters as LimeCoreCapabilityAdapters; } diff --git a/src/features/agent-app/sdk/capabilityCatalog.ts b/src/features/agent-app/sdk/capabilityCatalog.ts new file mode 100644 index 000000000..6cc3f8dd5 --- /dev/null +++ b/src/features/agent-app/sdk/capabilityCatalog.ts @@ -0,0 +1,502 @@ +import type { CapabilityImplementation } from "../types"; + +export type LimeCapabilityGroup = + | "app_surface" + | "data" + | "agent_runtime" + | "governance" + | "integration" + | "observability"; + +export type LimeCapabilityStage = "current" | "preview" | "planned"; + +export type LimeCapabilityOwner = + | "desktop_host" + | "agent_runtime" + | "tool_runtime" + | "knowledge_runtime" + | "artifact_runtime" + | "policy_runtime" + | "cloud_overlay"; + +export interface LimeCapabilityDefinition { + readonly name: string; + readonly version: string; + readonly group: LimeCapabilityGroup; + readonly stage: LimeCapabilityStage; + readonly owner: LimeCapabilityOwner; + readonly methods: readonly string[]; + readonly summary: string; + readonly appResponsibility: string; + readonly limeResponsibility: string; + readonly profile: { + readonly mock?: CapabilityImplementation; + readonly adapter?: CapabilityImplementation; + }; +} + +export interface LimeCapabilityProfileEntry { + readonly version: string; + readonly enabled: boolean; + readonly implementation: CapabilityImplementation; +} + +export const LIME_CAPABILITY_DEFINITIONS = [ + { + name: "lime.ui", + version: "0.3.0", + group: "app_surface", + stage: "current", + owner: "desktop_host", + methods: [ + "toast", + "navigate", + "openExternal", + "download", + "getSnapshot", + "openAgentRun", + "updateAgentRun", + "closeAgentRun", + ], + summary: "Lime 桌面壳的提示、导航、下载、主题、快照和统一 Agent Run UI 能力。", + appResponsibility: "决定业务页面如何展示和何时触发 Host action。", + limeResponsibility: "校验入口、同步主题语言、执行受控导航和下载,并提供通用 AI 运行现场。", + profile: { mock: "mock", adapter: "mock" }, + }, + { + name: "lime.storage", + version: "0.3.0", + group: "data", + stage: "current", + owner: "desktop_host", + methods: ["get", "set", "list", "delete"], + summary: "App namespace 下的结构化业务状态和轻量数据存储。", + appResponsibility: "定义业务对象、schema 和写回时机。", + limeResponsibility: "隔离 namespace、持久化数据、附加 App provenance。", + profile: { mock: "mock", adapter: "adapter" }, + }, + { + name: "lime.files", + version: "0.3.0", + group: "data", + stage: "current", + owner: "desktop_host", + methods: ["pick", "readRef", "parse"], + summary: "用户授权文件、file ref 读取和基础解析入口。", + appResponsibility: "声明需要的文件类型并把解析结果映射为业务草稿。", + limeResponsibility: "处理文件权限、引用生命周期、解析器和安全边界。", + profile: { mock: "mock", adapter: "mock" }, + }, + { + name: "lime.agent", + version: "0.3.0", + group: "agent_runtime", + stage: "current", + owner: "agent_runtime", + methods: [ + "startTask", + "streamTask", + "getTask", + "cancelTask", + "retryTask", + "submitHostResponse", + "listTasks", + ], + summary: "App-scoped Agent task、流式过程、追问、确认、取消和重试。", + appResponsibility: "组装业务输入、期望产物、人工确认和写回目标。", + limeResponsibility: + "复用 AgentRuntime、Skills、Tools、Evidence、模型和队列。", + profile: { mock: "mock", adapter: "adapter" }, + }, + { + name: "lime.knowledge", + version: "0.3.0", + group: "data", + stage: "current", + owner: "knowledge_runtime", + methods: ["search", "bindStatus", "bind", "export"], + summary: "项目知识、App knowledge binding、检索和版本导出。", + appResponsibility: "选择业务知识空间、解释检索结果并落到业务对象。", + limeResponsibility: "维护知识索引、权限、版本和引用 provenance。", + profile: { mock: "mock", adapter: "adapter" }, + }, + { + name: "lime.tools", + version: "0.3.0", + group: "agent_runtime", + stage: "current", + owner: "tool_runtime", + methods: ["invoke", "getProgress"], + summary: "Tool Broker / ToolHub 的受控工具调用与长任务状态。", + appResponsibility: "声明工具需求并消费结构化结果。", + limeResponsibility: "执行工具权限、审计、进度、超时和结果归一化。", + profile: { mock: "mock", adapter: "mock" }, + }, + { + name: "lime.artifacts", + version: "0.3.0", + group: "data", + stage: "current", + owner: "artifact_runtime", + methods: ["create", "open", "export", "list"], + summary: "产物创建、打开、导出、列表和 provenance。", + appResponsibility: "定义产物类型、标题、内容结构和业务状态联动。", + limeResponsibility: "持久化产物、管理 viewer/export、记录来源。", + profile: { mock: "mock", adapter: "adapter" }, + }, + { + name: "lime.workflow", + version: "0.3.0", + group: "agent_runtime", + stage: "current", + owner: "agent_runtime", + methods: ["start", "checkpoint", "awaitHuman"], + summary: "App workflow、checkpoint、后台任务和人类确认。", + appResponsibility: "定义业务步骤、状态机和人工介入点。", + limeResponsibility: "托管运行状态、恢复、权限和 Host response。", + profile: { mock: "mock", adapter: "mock" }, + }, + { + name: "lime.policy", + version: "0.3.0", + group: "governance", + stage: "current", + owner: "policy_runtime", + methods: ["check", "requestPermission"], + summary: "权限、风险、成本、数据和企业策略检查。", + appResponsibility: "说明为什么需要能力并响应被拒绝状态。", + limeResponsibility: "统一执行策略、授权、审计和降级。", + profile: {}, + }, + { + name: "lime.secrets", + version: "0.3.0", + group: "governance", + stage: "current", + owner: "policy_runtime", + methods: ["getRef", "requestBinding"], + summary: "OAuth、API key、外部平台凭证和 secret ref。", + appResponsibility: "声明凭证用途,只保存 ref,不读取明文。", + limeResponsibility: "托管凭证、授权弹窗、轮换和最小权限访问。", + profile: {}, + }, + { + name: "lime.evidence", + version: "0.3.0", + group: "observability", + stage: "current", + owner: "artifact_runtime", + methods: ["record", "linkArtifact", "list"], + summary: "来源、引用、工具调用、评估和发布证据。", + appResponsibility: "声明业务证据类型并把证据挂到产物/任务。", + limeResponsibility: "保证 evidence 可追溯、可导出、可审计。", + profile: { mock: "mock", adapter: "adapter" }, + }, + { + name: "lime.events", + version: "0.3.0", + group: "app_surface", + stage: "preview", + owner: "desktop_host", + methods: ["emit", "subscribe", "unsubscribe", "listSubscriptions"], + summary: "App UI、worker、Host 和 runtime 间的事件通道。", + appResponsibility: "消费业务事件并避免私有 postMessage 协议。", + limeResponsibility: "路由事件、隔离 namespace、控制订阅生命周期。", + profile: {}, + }, + { + name: "lime.capabilities", + version: "0.3.0", + group: "governance", + stage: "preview", + owner: "desktop_host", + methods: ["list", "get", "getProfile"], + summary: "Host capability catalog、版本、可用性和 readiness 摘要。", + appResponsibility: "按 profile 决定 UI 降级,不猜测底层实现。", + limeResponsibility: "发布单一能力目录,阻止 App 自建能力表。", + profile: {}, + }, + { + name: "lime.models", + version: "0.3.0", + group: "agent_runtime", + stage: "preview", + owner: "agent_runtime", + methods: ["list", "select", "getRouting", "estimateCost"], + summary: "模型列表、模型路由、能力约束和预估成本。", + appResponsibility: "表达任务偏好和质量/成本约束,不保存 provider 密钥。", + limeResponsibility: "统一模型事实源、路由、Provider 能力和成本估算。", + profile: { adapter: "adapter" }, + }, + { + name: "lime.usage", + version: "0.3.0", + group: "observability", + stage: "preview", + owner: "agent_runtime", + methods: ["getTokenUsage", "getCostSummary", "getBudget"], + summary: "Token、费用、预算、任务和 App 级用量归因。", + appResponsibility: "展示业务任务成本并响应预算拦截。", + limeResponsibility: "从 runtime request telemetry 聚合真实用量。", + profile: { adapter: "adapter" }, + }, + { + name: "lime.memory", + version: "0.3.0", + group: "agent_runtime", + stage: "preview", + owner: "agent_runtime", + methods: ["query", "write", "compact", "getStatus"], + summary: "工作记忆、长期记忆、团队记忆和上下文压缩。", + appResponsibility: "声明业务记忆意图和可写范围。", + limeResponsibility: "复用 memory_runtime / unified_memory 主链并处理压缩。", + profile: { adapter: "adapter" }, + }, + { + name: "lime.skills", + version: "0.3.0", + group: "agent_runtime", + stage: "preview", + owner: "agent_runtime", + methods: ["list", "resolve", "bind", "invoke", "getInvocation"], + summary: "Skill 注册、发现、绑定、启用状态和调用过程。", + appResponsibility: "声明必需 Skill 和业务场景,不复制 Skill runtime。", + limeResponsibility: + "管理 Skill catalog、workspace binding、runtime gate 和 evidence。", + profile: { adapter: "adapter" }, + }, + { + name: "lime.mcp", + version: "0.3.0", + group: "integration", + stage: "preview", + owner: "tool_runtime", + methods: ["listServers", "listTools", "invoke"], + summary: "MCP server、tool inventory 和受控调用。", + appResponsibility: "声明需要的 MCP capability,不直接连接 server。", + limeResponsibility: "复用 MCP bridge runtime、命名、权限和审计。", + profile: { adapter: "adapter" }, + }, + { + name: "lime.browser", + version: "0.3.0", + group: "integration", + stage: "preview", + owner: "tool_runtime", + methods: ["open", "navigate", "extract", "screenshot", "close"], + summary: "浏览器自动化、网页读取、截图和会话隔离。", + appResponsibility: "表达业务采集目标和用户授权语义。", + limeResponsibility: "托管浏览器 profile、权限、工具结果和回放证据。", + profile: { adapter: "adapter" }, + }, + { + name: "lime.search", + version: "0.3.0", + group: "integration", + stage: "preview", + owner: "tool_runtime", + methods: ["query", "deepResearch", "getRun"], + summary: "网页搜索、深度研究、来源和运行状态。", + appResponsibility: "给出业务问题、筛选规则和结果消费方式。", + limeResponsibility: "统一搜索 provider、来源去重、引用和成本。", + profile: { adapter: "adapter" }, + }, + { + name: "lime.documents", + version: "0.3.0", + group: "data", + stage: "preview", + owner: "tool_runtime", + methods: ["parse", "export", "transform", "summarize"], + summary: "PDF、Word、Markdown、PPT 等文档解析、转换和导出。", + appResponsibility: "定义业务文档类型和结构化落点。", + limeResponsibility: "执行解析器、格式转换、文件权限和 evidence。", + profile: { adapter: "adapter" }, + }, + { + name: "lime.media", + version: "0.3.0", + group: "integration", + stage: "preview", + owner: "tool_runtime", + methods: ["generateImage", "editImage", "transcribe", "synthesizeVoice"], + summary: "图片、音频、语音、视频素材的生成和处理。", + appResponsibility: "给出业务 brief、尺寸、风格和交付约束。", + limeResponsibility: "托管媒体 runtime、文件产物、安全策略和成本。", + profile: { adapter: "adapter" }, + }, + { + name: "lime.terminal", + version: "0.3.0", + group: "integration", + stage: "preview", + owner: "tool_runtime", + methods: ["run", "getRun", "cancel"], + summary: "命令执行、日志、取消、sandbox 和审批。", + appResponsibility: "声明命令目的和输入,不能绕过审批。", + limeResponsibility: "执行 sandbox、approval、日志和危险操作拦截。", + profile: { adapter: "adapter" }, + }, + { + name: "lime.tasks", + version: "0.3.0", + group: "observability", + stage: "preview", + owner: "agent_runtime", + methods: ["list", "get", "cancel", "subscribe"], + summary: "跨 App / runtime 的后台任务、队列、状态和订阅。", + appResponsibility: "展示与本 App 相关的任务,不维护第二套队列。", + limeResponsibility: "统一任务中心、状态恢复、事件订阅和审计。", + profile: {}, + }, + { + name: "lime.settings", + version: "0.3.0", + group: "governance", + stage: "preview", + owner: "desktop_host", + methods: ["get", "set", "list"], + summary: "App 可见设置、workspace overlay 和 tenant 默认值。", + appResponsibility: "只读或请求修改自己的配置域。", + limeResponsibility: "统一设置 schema、权限、overlay 和迁移。", + profile: {}, + }, + { + name: "lime.workspace", + version: "0.3.0", + group: "app_surface", + stage: "preview", + owner: "desktop_host", + methods: ["getCurrent", "list", "open", "getPathRef"], + summary: "当前 workspace、路径引用、打开入口和工作区上下文。", + appResponsibility: "围绕当前 workspace 展示业务状态。", + limeResponsibility: "管理 workspace 身份、路径封装和跨平台兼容。", + profile: {}, + }, + { + name: "lime.context", + version: "0.3.0", + group: "agent_runtime", + stage: "preview", + owner: "agent_runtime", + methods: ["getSnapshot", "attach", "detach"], + summary: "会话上下文、选中资源、当前任务和可附加上下文。", + appResponsibility: "把业务选择显式提交给 runtime。", + limeResponsibility: "维护 session/thread/turn 上下文和压缩边界。", + profile: { adapter: "adapter" }, + }, + { + name: "lime.connectors", + version: "0.3.0", + group: "integration", + stage: "preview", + owner: "cloud_overlay", + methods: ["list", "requestAuth", "getStatus", "invoke"], + summary: "外部系统连接器、授权状态和受控集成调用。", + appResponsibility: "声明业务连接需求和用户动作。", + limeResponsibility: "托管 OAuth、secret、tenant policy 和审计。", + profile: { adapter: "adapter" }, + }, + { + name: "lime.automation", + version: "0.3.0", + group: "agent_runtime", + stage: "preview", + owner: "agent_runtime", + methods: ["startJob", "getJob", "cancelJob"], + summary: "自动化 job、周期任务和服务型 Skill 编排。", + appResponsibility: "定义业务触发、输入和终止条件。", + limeResponsibility: "统一 job runtime、队列、权限、证据和恢复。", + profile: {}, + }, + { + name: "lime.review", + version: "0.3.0", + group: "governance", + stage: "preview", + owner: "policy_runtime", + methods: ["requestDecision", "submitDecision", "listPending"], + summary: "人工审核、风险确认、发布门禁和决策记录。", + appResponsibility: "把审核嵌入业务 UI 并处理拒绝/重试。", + limeResponsibility: "保留审核证据、权限、审计和发布门禁。", + profile: {}, + }, +] as const satisfies readonly LimeCapabilityDefinition[]; + +export type LimeCapabilityDefinitionRecord = + (typeof LIME_CAPABILITY_DEFINITIONS)[number]; +export type LimeCapabilityName = LimeCapabilityDefinitionRecord["name"]; +export type LimeCapabilityMethodName = Extract< + LimeCapabilityDefinitionRecord, + { name: Name } +>["methods"][number]; +export type LimeCapabilityAdapterKey< + Name extends LimeCapabilityName = LimeCapabilityName, +> = Name extends `lime.${infer Key}` ? Key : never; + +export const LIME_CAPABILITY_NAMES = LIME_CAPABILITY_DEFINITIONS.map( + (definition) => definition.name, +) as readonly LimeCapabilityName[]; + +export const LIME_CAPABILITY_GROUPS = Array.from( + new Set(LIME_CAPABILITY_DEFINITIONS.map((definition) => definition.group)), +) as readonly LimeCapabilityGroup[]; + +export function getLimeCapabilityDefinition( + name: LimeCapabilityName, +): LimeCapabilityDefinitionRecord { + const definition = LIME_CAPABILITY_DEFINITIONS.find( + (item) => item.name === name, + ); + if (!definition) { + throw new Error(`Unknown Lime capability: ${name}`); + } + return definition; +} + +export function getLimeCapabilityAdapterKey( + name: Name, +): LimeCapabilityAdapterKey { + return name.slice("lime.".length) as LimeCapabilityAdapterKey; +} + +export function buildLimeCapabilityProfileEntries( + resolveImplementation?: ( + definition: LimeCapabilityDefinitionRecord, + ) => CapabilityImplementation | undefined, +): Record { + return Object.fromEntries( + LIME_CAPABILITY_DEFINITIONS.map((definition) => { + const implementation = resolveImplementation?.(definition) ?? "none"; + return [ + definition.name, + { + version: definition.version, + enabled: implementation !== "none", + implementation, + }, + ]; + }), + ) as Record; +} + +export function buildLimeCapabilityProfileEntriesForMode( + mode: "base" | "mock" | "adapter", +): Record { + if (mode === "base") { + return buildLimeCapabilityProfileEntries(); + } + return buildLimeCapabilityProfileEntries((definition) => { + const profile: LimeCapabilityDefinition["profile"] = definition.profile; + return profile[mode] ?? "none"; + }); +} + +export function listEnabledLimeCapabilityNamesForMode( + mode: "mock" | "adapter", +): LimeCapabilityName[] { + return LIME_CAPABILITY_DEFINITIONS.filter((definition) => { + const profile: LimeCapabilityDefinition["profile"] = definition.profile; + return profile[mode] !== undefined; + }).map((definition) => definition.name); +} diff --git a/src/features/agent-app/sdk/capabilityContract.test.ts b/src/features/agent-app/sdk/capabilityContract.test.ts index 2c02bfcba..84cf8222d 100644 --- a/src/features/agent-app/sdk/capabilityContract.test.ts +++ b/src/features/agent-app/sdk/capabilityContract.test.ts @@ -1,23 +1,34 @@ import { describe, expect, it } from "vitest"; import type { AgentAppTaskRecord } from "../types"; +import { buildAdapterCapabilityProfile } from "../adapters/adapterCapabilityProfile"; +import { p0HostCapabilityProfile } from "../readiness/hostCapabilityProfile"; import { AgentAppCapabilityError, normalizeLimeCapabilityErrorCode, toLimeCapabilityError, } from "./capabilityErrors"; +import { + LIME_CAPABILITY_DEFINITIONS, + LIME_CAPABILITY_NAMES, + buildLimeCapabilityProfileEntriesForMode, + listEnabledLimeCapabilityNamesForMode, +} from "./capabilityCatalog"; import { buildLimeCapabilityInvokeProvenance, buildLimeCapabilityInvokeRequest, createLimeCapabilityInvoker, createMockLimeCapabilityTransport, } from "./capabilityContract"; +import { buildMockCapabilityProfile } from "./mockCapabilityProfile"; const provenance = buildLimeCapabilityInvokeProvenance({ sourceKind: "agent_app", appId: "content-factory-app", appVersion: "1.0.0", - packageHash: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - manifestHash: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + packageHash: + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + manifestHash: + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", entryKey: "dashboard", workflowRunId: "run-1", workspaceId: "workspace-1", @@ -57,6 +68,72 @@ function buildTaskRecord(input: { title: string }): AgentAppTaskRecord { } describe("P18 typed Capability SDK contract", () => { + it("应把 Lime 全量能力目录作为 SDK / readiness / profile 的单一事实源", () => { + const catalogNames = LIME_CAPABILITY_DEFINITIONS.map( + (definition) => definition.name, + ); + + expect(new Set(catalogNames).size).toBe(catalogNames.length); + expect(LIME_CAPABILITY_NAMES).toEqual(catalogNames); + expect(catalogNames).toEqual( + expect.arrayContaining([ + "lime.ui", + "lime.storage", + "lime.files", + "lime.agent", + "lime.knowledge", + "lime.tools", + "lime.artifacts", + "lime.workflow", + "lime.policy", + "lime.secrets", + "lime.evidence", + "lime.events", + "lime.capabilities", + "lime.models", + "lime.usage", + "lime.memory", + "lime.skills", + "lime.mcp", + "lime.browser", + "lime.search", + "lime.documents", + "lime.media", + "lime.terminal", + "lime.tasks", + "lime.settings", + "lime.workspace", + "lime.context", + "lime.connectors", + "lime.automation", + "lime.review", + ]), + ); + expect(Object.keys(p0HostCapabilityProfile.capabilities).sort()).toEqual( + [...catalogNames].sort(), + ); + expect(p0HostCapabilityProfile.capabilities["lime.policy"]).toMatchObject({ + enabled: false, + implementation: "none", + }); + expect(p0HostCapabilityProfile.capabilities["lime.secrets"]).toMatchObject({ + enabled: false, + implementation: "none", + }); + expect(buildLimeCapabilityProfileEntriesForMode("base")).toEqual( + p0HostCapabilityProfile.capabilities, + ); + expect( + Object.keys(buildMockCapabilityProfile().capabilities).sort(), + ).toEqual([...catalogNames].sort()); + expect( + Object.keys(buildAdapterCapabilityProfile().capabilities).sort(), + ).toEqual([...catalogNames].sort()); + expect(listEnabledLimeCapabilityNamesForMode("adapter")).toEqual( + expect.arrayContaining(["lime.agent", "lime.storage", "lime.evidence"]), + ); + }); + it("应构造带 app provenance 的 typed capability invoke envelope", () => { const request = buildLimeCapabilityInvokeRequest({ capability: "lime.agent", diff --git a/src/features/agent-app/sdk/capabilityContract.ts b/src/features/agent-app/sdk/capabilityContract.ts index 2cea1108e..b2a00cd2e 100644 --- a/src/features/agent-app/sdk/capabilityContract.ts +++ b/src/features/agent-app/sdk/capabilityContract.ts @@ -15,22 +15,9 @@ import { type LimeCapabilityError, type LimeCapabilityErrorContext, } from "./capabilityErrors"; - -export const LIME_CAPABILITY_NAMES = [ - "lime.ui", - "lime.storage", - "lime.files", - "lime.agent", - "lime.knowledge", - "lime.tools", - "lime.artifacts", - "lime.workflow", - "lime.policy", - "lime.secrets", - "lime.evidence", -] as const; - -export type LimeCapabilityName = (typeof LIME_CAPABILITY_NAMES)[number]; +import type { LimeCapabilityName } from "./capabilityCatalog"; +export { LIME_CAPABILITY_NAMES } from "./capabilityCatalog"; +export type { LimeCapabilityName } from "./capabilityCatalog"; export interface LimeCapabilityInvokeProvenance { appId: string; @@ -93,6 +80,50 @@ export interface LimeCapabilityContractMap { args: undefined; value: unknown; }; + openAgentRun: { + args: { + taskId?: string; + bridgeAction?: string; + title?: string; + mode?: "drawer" | "modal" | "page"; + expectedOutput?: unknown; + runtimeProcess?: unknown; + task?: unknown; + snapshot?: unknown; + events?: unknown[]; + }; + value: { + opened: true; + surface: "host_agent_run"; + mode: "drawer" | "modal" | "page"; + taskId?: string; + }; + }; + updateAgentRun: { + args: { + taskId?: string; + bridgeAction?: string; + title?: string; + runtimeProcess?: unknown; + task?: unknown; + snapshot?: unknown; + events?: unknown[]; + runtimeFacts?: unknown; + }; + value: { + updated: true; + surface: "host_agent_run"; + taskId?: string; + }; + }; + closeAgentRun: { + args: { taskId?: string; bridgeAction?: string } | undefined; + value: { + closed: true; + surface: "host_agent_run"; + taskId?: string; + }; + }; }; "lime.storage": { get: { args: { key: string }; value: unknown | null }; @@ -103,6 +134,10 @@ export interface LimeCapabilityContractMap { "lime.files": { pick: { args: { accept?: string[]; multiple?: boolean }; value: unknown }; readRef: { args: { ref: string }; value: unknown }; + parse: { + args: { ref: string; parser?: string; outputSchema?: unknown }; + value: unknown; + }; }; "lime.agent": { startTask: { args: AgentAppTaskRequest; value: AgentAppTaskRecord }; @@ -122,6 +157,8 @@ export interface LimeCapabilityContractMap { value: AgentAppKnowledgeSearchResult; }; bindStatus: { args: { key: string }; value: unknown }; + bind: { args: { key: string; mode?: string }; value: unknown }; + export: { args: { key: string; format?: string }; value: unknown }; }; "lime.tools": { invoke: { args: { tool: string; input?: unknown }; value: unknown }; @@ -134,6 +171,10 @@ export interface LimeCapabilityContractMap { }; open: { args: { artifactId: string }; value: unknown }; export: { args: { artifactId: string; format?: string }; value: unknown }; + list: { + args: { kind?: string; limit?: number } | undefined; + value: AgentAppArtifactRecord[]; + }; }; "lime.workflow": { start: { args: { workflowKey: string; input?: unknown }; value: unknown }; @@ -166,12 +207,173 @@ export interface LimeCapabilityContractMap { args: { evidenceId: string; artifactId: string }; value: unknown; }; + list: { + args: { kind?: string; limit?: number } | undefined; + value: AgentAppEvidenceRecord[]; + }; + }; + "lime.events": { + emit: { args: { topic: string; payload?: unknown }; value: unknown }; + subscribe: { args: { topic: string }; value: unknown }; + unsubscribe: { args: { subscriptionId: string }; value: unknown }; + listSubscriptions: { args: undefined; value: unknown }; + }; + "lime.capabilities": { + list: { args: undefined; value: unknown }; + get: { args: { capability: string }; value: unknown }; + getProfile: { args: undefined; value: unknown }; + }; + "lime.models": { + list: { args: { taskKind?: string } | undefined; value: unknown }; + select: { + args: { taskKind: string; constraints?: unknown }; + value: unknown; + }; + getRouting: { args: { taskKind?: string } | undefined; value: unknown }; + estimateCost: { + args: { taskKind: string; input?: unknown }; + value: unknown; + }; + }; + "lime.usage": { + getTokenUsage: { + args: { taskId?: string; window?: string } | undefined; + value: unknown; + }; + getCostSummary: { + args: { taskId?: string; window?: string } | undefined; + value: unknown; + }; + getBudget: { args: { scope?: string } | undefined; value: unknown }; + }; + "lime.memory": { + query: { args: { query: string; scope?: string }; value: unknown }; + write: { args: { scope: string; value: unknown }; value: unknown }; + compact: { + args: { scope?: string; reason?: string } | undefined; + value: unknown; + }; + getStatus: { args: { scope?: string } | undefined; value: unknown }; + }; + "lime.skills": { + list: { args: { kind?: string } | undefined; value: unknown }; + resolve: { args: { skillId: string }; value: unknown }; + bind: { args: { skillId: string; reason?: string }; value: unknown }; + invoke: { args: { skillId: string; input?: unknown }; value: unknown }; + getInvocation: { args: { invocationId: string }; value: unknown }; + }; + "lime.mcp": { + listServers: { args: undefined; value: unknown }; + listTools: { args: { serverId?: string } | undefined; value: unknown }; + invoke: { args: { tool: string; input?: unknown }; value: unknown }; + }; + "lime.browser": { + open: { args: { url?: string; profile?: string }; value: unknown }; + navigate: { args: { sessionId: string; url: string }; value: unknown }; + extract: { args: { sessionId: string; selector?: string }; value: unknown }; + screenshot: { + args: { sessionId: string; fullPage?: boolean }; + value: unknown; + }; + close: { args: { sessionId: string }; value: unknown }; + }; + "lime.search": { + query: { args: { query: string; limit?: number }; value: unknown }; + deepResearch: { args: { query: string; depth?: number }; value: unknown }; + getRun: { args: { runId: string }; value: unknown }; + }; + "lime.documents": { + parse: { args: { ref: string; outputSchema?: unknown }; value: unknown }; + export: { args: { artifactId: string; format: string }; value: unknown }; + transform: { + args: { ref: string; operation: string; options?: unknown }; + value: unknown; + }; + summarize: { args: { ref: string; instruction?: string }; value: unknown }; + }; + "lime.media": { + generateImage: { + args: { prompt: string; options?: unknown }; + value: unknown; + }; + editImage: { + args: { ref: string; prompt: string; options?: unknown }; + value: unknown; + }; + transcribe: { args: { ref: string; options?: unknown }; value: unknown }; + synthesizeVoice: { + args: { text: string; voice?: string; options?: unknown }; + value: unknown; + }; + }; + "lime.terminal": { + run: { + args: { command: string; cwdRef?: string; reason?: string }; + value: unknown; + }; + getRun: { args: { runId: string }; value: unknown }; + cancel: { args: { runId: string }; value: unknown }; + }; + "lime.tasks": { + list: { + args: { status?: string; limit?: number } | undefined; + value: unknown; + }; + get: { args: { taskId: string }; value: unknown }; + cancel: { args: { taskId: string }; value: unknown }; + subscribe: { args: { taskId: string }; value: unknown }; + }; + "lime.settings": { + get: { args: { key: string }; value: unknown }; + set: { + args: { key: string; value: unknown; reason?: string }; + value: unknown; + }; + list: { args: { namespace?: string } | undefined; value: unknown }; + }; + "lime.workspace": { + getCurrent: { args: undefined; value: unknown }; + list: { args: undefined; value: unknown }; + open: { args: { workspaceId: string }; value: unknown }; + getPathRef: { args: { purpose: string; path?: string }; value: unknown }; + }; + "lime.context": { + getSnapshot: { args: { scope?: string } | undefined; value: unknown }; + attach: { args: { ref: string; kind?: string }; value: unknown }; + detach: { args: { ref: string }; value: unknown }; + }; + "lime.connectors": { + list: { args: { kind?: string } | undefined; value: unknown }; + requestAuth: { + args: { connectorId: string; reason?: string }; + value: unknown; + }; + getStatus: { args: { connectorId: string }; value: unknown }; + invoke: { + args: { connectorId: string; action: string; input?: unknown }; + value: unknown; + }; + }; + "lime.automation": { + startJob: { args: { jobKind: string; input?: unknown }; value: unknown }; + getJob: { args: { jobId: string }; value: unknown }; + cancelJob: { args: { jobId: string }; value: unknown }; + }; + "lime.review": { + requestDecision: { + args: { subject: string; payload?: unknown }; + value: unknown; + }; + submitDecision: { + args: { decisionId: string; decision: string; note?: string }; + value: unknown; + }; + listPending: { args: { subject?: string } | undefined; value: unknown }; }; } -export type LimeCapabilityMethod< - Capability extends LimeCapabilityName, -> = Extract; +export type LimeCapabilityMethod = + Extract; type LimeCapabilitySpec< Capability extends LimeCapabilityName, diff --git a/src/features/agent-app/sdk/index.ts b/src/features/agent-app/sdk/index.ts index 1f529f71b..008e731c6 100644 --- a/src/features/agent-app/sdk/index.ts +++ b/src/features/agent-app/sdk/index.ts @@ -11,6 +11,25 @@ export type { LimeCapabilityErrorCode, LimeCapabilityErrorContext, } from "./capabilityErrors"; +export { + LIME_CAPABILITY_DEFINITIONS, + LIME_CAPABILITY_GROUPS, + buildLimeCapabilityProfileEntries, + buildLimeCapabilityProfileEntriesForMode, + getLimeCapabilityAdapterKey, + getLimeCapabilityDefinition, + listEnabledLimeCapabilityNamesForMode, +} from "./capabilityCatalog"; +export type { + LimeCapabilityDefinition, + LimeCapabilityAdapterKey, + LimeCapabilityDefinitionRecord, + LimeCapabilityGroup, + LimeCapabilityMethodName, + LimeCapabilityOwner, + LimeCapabilityProfileEntry, + LimeCapabilityStage, +} from "./capabilityCatalog"; export { LIME_CAPABILITY_NAMES, buildLimeCapabilityInvokeProvenance, @@ -45,13 +64,37 @@ export type { CreateLimeCoreCapabilityAdaptersOptions, LimeAgentCapabilityAdapter, LimeArtifactsCapabilityAdapter, + LimeAutomationCapabilityAdapter, + LimeBrowserCapabilityAdapter, + LimeCapabilitiesCapabilityAdapter, LimeCapabilityAdapterCallOptions, + LimeCapabilityAdapter, + LimeConnectorsCapabilityAdapter, + LimeContextCapabilityAdapter, LimeCoreCapabilityAdapters, + LimeDocumentsCapabilityAdapter, LimeEvidenceCapabilityAdapter, + LimeEventsCapabilityAdapter, + LimeFilesCapabilityAdapter, LimeKnowledgeCapabilityAdapter, + LimeMcpCapabilityAdapter, + LimeMediaCapabilityAdapter, + LimeMemoryCapabilityAdapter, + LimeModelsCapabilityAdapter, + LimePolicyCapabilityAdapter, + LimeReviewCapabilityAdapter, + LimeSearchCapabilityAdapter, + LimeSecretsCapabilityAdapter, + LimeSettingsCapabilityAdapter, + LimeSkillsCapabilityAdapter, LimeStorageCapabilityAdapter, + LimeTasksCapabilityAdapter, + LimeTerminalCapabilityAdapter, LimeToolsCapabilityAdapter, LimeUiCapabilityAdapter, + LimeUsageCapabilityAdapter, + LimeWorkflowCapabilityAdapter, + LimeWorkspaceCapabilityAdapter, } from "./capabilityAdapters"; export { LIME_AGENT_APP_BRIDGE_PROTOCOL, diff --git a/src/features/agent-app/sdk/mockCapabilityProfile.ts b/src/features/agent-app/sdk/mockCapabilityProfile.ts index 8de8fc47b..87a3fe540 100644 --- a/src/features/agent-app/sdk/mockCapabilityProfile.ts +++ b/src/features/agent-app/sdk/mockCapabilityProfile.ts @@ -1,18 +1,7 @@ import { resolveAgentAppHostFlags } from "../featureFlag"; import { p0HostCapabilityProfile } from "../readiness/hostCapabilityProfile"; import type { AgentAppHostFlags, HostCapabilityProfile } from "../types"; - -const MOCK_CAPABILITIES = [ - "lime.ui", - "lime.storage", - "lime.files", - "lime.artifacts", - "lime.evidence", - "lime.agent", - "lime.knowledge", - "lime.tools", - "lime.workflow", -] as const; +import { buildLimeCapabilityProfileEntriesForMode } from "./capabilityCatalog"; export function buildMockCapabilityProfile( flagOverrides: Partial = {}, @@ -22,21 +11,10 @@ export function buildMockCapabilityProfile( labEnabled: true, mockSdkEnabled: true, }); - const capabilities: HostCapabilityProfile["capabilities"] = { - ...p0HostCapabilityProfile.capabilities, - }; - - MOCK_CAPABILITIES.forEach((capability) => { - capabilities[capability] = { - version: capabilities[capability]?.version ?? "0.3.0", - enabled: true, - implementation: "mock", - }; - }); return { ...p0HostCapabilityProfile, - capabilities, + capabilities: buildLimeCapabilityProfileEntriesForMode("mock"), featureFlags, }; } diff --git a/src/features/agent-app/sdk/publicSdkSurface.test.ts b/src/features/agent-app/sdk/publicSdkSurface.test.ts index b479412c0..558f02174 100644 --- a/src/features/agent-app/sdk/publicSdkSurface.test.ts +++ b/src/features/agent-app/sdk/publicSdkSurface.test.ts @@ -7,6 +7,7 @@ import { describe, expect, it } from "vitest"; import { LIME_AGENT_APP_BRIDGE_PROTOCOL, LIME_AGENT_APP_BRIDGE_VERSION, + LIME_CAPABILITY_DEFINITIONS, LIME_CAPABILITY_NAMES, MockCapabilityHost, createLimeCoreCapabilityAdapters, @@ -27,6 +28,13 @@ describe("agent app SDK public surface", () => { expect(LIME_AGENT_APP_BRIDGE_PROTOCOL).toBe("lime.agentApp.bridge"); expect(LIME_AGENT_APP_BRIDGE_VERSION).toBe(1); expect(LIME_CAPABILITY_NAMES).toContain("lime.agent"); + expect(LIME_CAPABILITY_DEFINITIONS).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: "lime.models" }), + expect.objectContaining({ name: "lime.skills" }), + expect.objectContaining({ name: "lime.usage" }), + ]), + ); expect(isLimeCapabilityErrorCode("permission_denied")).toBe(true); expect(normalizeLimeCapabilityErrorCode("missing")).toBe("upstream_failed"); expect(typeof createLimeCoreCapabilityAdapters).toBe("function"); @@ -44,7 +52,9 @@ describe("agent app SDK public surface", () => { ); expect(publicSdkSurface).not.toHaveProperty("AgentRuntimeCapabilityHost"); expect(publicSdkSurface).not.toHaveProperty("WorkflowRuntimeHost"); - expect(publicSdkSurface).not.toHaveProperty("createAgentAppCapabilityDispatcher"); + expect(publicSdkSurface).not.toHaveProperty( + "createAgentAppCapabilityDispatcher", + ); }); it("源码层不从 UI、安装、运行时或 adapter 内部层转导出", () => { diff --git a/src/features/agent-app/types.ts b/src/features/agent-app/types.ts index 225fafa94..ff1c5cf69 100644 --- a/src/features/agent-app/types.ts +++ b/src/features/agent-app/types.ts @@ -205,6 +205,11 @@ export interface AppManifest { overlayTemplates?: OverlayTemplateDeclaration[]; ui?: UiDeclaration; lifecycle?: LifecycleDeclaration; + agentRuntime?: unknown; + requirements?: unknown; + boundary?: unknown; + integrations?: unknown; + operations?: unknown; } export interface NormalizedRequires { @@ -250,7 +255,7 @@ export interface NormalizedAppEntry { } export interface NormalizedAppManifest { - manifestVersion: "0.2" | "0.3"; + manifestVersion: "0.2" | "0.3" | "0.5" | "0.6" | "0.7"; appId: string; displayName: string; version: string; @@ -276,6 +281,11 @@ export interface NormalizedAppManifest { overlayTemplates: OverlayTemplateDeclaration[]; ui?: UiDeclaration; lifecycle: LifecycleDeclaration; + agentRuntime?: unknown; + requirements?: unknown; + boundary?: unknown; + integrations?: unknown; + operations?: unknown; } export interface PackageIdentity { @@ -734,6 +744,10 @@ export interface AgentAppHostFlags { export interface HostCapabilityProfile { appRuntimeVersion: string; + standardVersions?: { + current: string; + compatible: string[]; + }; runtimeTargets: RuntimeTarget[]; capabilities: Record< string, @@ -743,6 +757,7 @@ export interface HostCapabilityProfile { implementation: CapabilityImplementation; } >; + agentRuntime?: unknown; featureFlags: AgentAppHostFlags; } @@ -912,6 +927,69 @@ export interface AgentAppTaskStreamEvent { refs?: string[]; } +export type AgentAppRuntimeProcessTimelineKind = + | "progress" + | "thinking" + | "output" + | "execution" + | "routing" + | "metrics" + | "skill" + | "tool" + | "artifact" + | "blocked" + | "warning" + | "completed"; + +export interface AgentAppRuntimeProcessTimelineItem { + kind: AgentAppRuntimeProcessTimelineKind; + title: string; + statusText: string; + message: string; + detail?: string; + meta?: string; + collapseKey?: string; +} + +export interface AgentAppRuntimeProcessModel { + provider: string; + model: string; + label: string; +} + +export interface AgentAppRuntimeProcessUsage { + inputTokens: number; + outputTokens: number; + totalTokens: number; + cachedInputTokens?: number; + cacheCreationInputTokens?: number; + [key: string]: unknown; +} + +export interface AgentAppRuntimeProcessCost { + estimatedTotalCost?: number; + estimatedCostClass?: string; + currency?: string; + [key: string]: unknown; +} + +export interface AgentAppRuntimeProcessView { + timeline: AgentAppRuntimeProcessTimelineItem[]; + streamText: string; + thinkingText: string; + executionText: string; + skillNames: string[]; + invokedSkillNames: string[]; + model: AgentAppRuntimeProcessModel; + usage: AgentAppRuntimeProcessUsage | null; + cost: AgentAppRuntimeProcessCost | null; + terminal: boolean; + collapsedByDefault: boolean; + routingCount: number; + executionCount: number; + artifactCount: number; +} + export interface AgentAppTaskKnowledgeBinding { key: string; mode?: "retrieval" | "data"; @@ -989,6 +1067,8 @@ export interface AgentAppTaskRecord { result?: unknown; trace: AgentAppTaskTraceEvent[]; events: AgentAppTaskStreamEvent[]; + runtimeProcess?: AgentAppRuntimeProcessView; + process?: AgentAppRuntimeProcessView; provenance: AgentAppProvenance; } diff --git a/src/features/agent-app/ui/AgentAppRuntimePage.test.tsx b/src/features/agent-app/ui/AgentAppRuntimePage.test.tsx index 6a8dd310e..8c6c8145e 100644 --- a/src/features/agent-app/ui/AgentAppRuntimePage.test.tsx +++ b/src/features/agent-app/ui/AgentAppRuntimePage.test.tsx @@ -49,6 +49,42 @@ const i18nMocks = vi.hoisted(() => ({ "agentApp.apps.runtime.openFailed": "App 打开失败", "agentApp.apps.runtime.retry": "重新打开", "agentApp.apps.runtime.unavailable": "该 Agent App 暂不可用。", + "agentApp.apps.runtime.agentRun.aria": "Host AI 运行面板", + "agentApp.apps.runtime.agentRun.badge": `${String(params?.app)} 的 AI 同事`, + "agentApp.apps.runtime.agentRun.titleFallback": "Lime AI 运行现场", + "agentApp.apps.runtime.agentRun.subtitle": + "思考、执行、Skill、模型、Token、费用和证据由 Lime Host 统一承载。", + "agentApp.apps.runtime.agentRun.close": "关闭 AI 运行面板", + "agentApp.apps.runtime.agentRun.expand": "查看运行现场", + "agentApp.apps.runtime.agentRun.collapse": "收起运行面板", + "agentApp.apps.runtime.agentRun.taskId": "任务 ID", + "agentApp.apps.runtime.agentRun.bridgeAction": "业务动作", + "agentApp.apps.runtime.agentRun.emptyValue": "待回写", + "agentApp.apps.runtime.agentRun.metric.model": "模型", + "agentApp.apps.runtime.agentRun.metric.tokens": "Token", + "agentApp.apps.runtime.agentRun.metric.cost": "费用", + "agentApp.apps.runtime.agentRun.metric.skills": "Skills", + "agentApp.apps.runtime.agentRun.facts.confirmations": "待确认", + "agentApp.apps.runtime.agentRun.facts.confirmations.empty": "暂无待确认项", + "agentApp.apps.runtime.agentRun.facts.confirmations.itemFallback": + "待确认事项", + "agentApp.apps.runtime.agentRun.facts.artifacts": "交付物", + "agentApp.apps.runtime.agentRun.facts.artifacts.empty": + "暂无交付物", + "agentApp.apps.runtime.agentRun.facts.artifacts.itemFallback": "交付物已生成", + "agentApp.apps.runtime.agentRun.facts.evidence": "证据", + "agentApp.apps.runtime.agentRun.facts.evidence.empty": "暂无证据", + "agentApp.apps.runtime.agentRun.facts.evidence.itemFallback": "证据已记录", + "agentApp.apps.runtime.agentRun.timeline.running": + "运行中,点击折叠或展开过程", + "agentApp.apps.runtime.agentRun.timeline.collapsed": + "运行过程已折叠,点击查看完整现场", + "agentApp.apps.runtime.agentRun.timeline.event": "运行事件", + "agentApp.apps.runtime.agentRun.timeline.empty": + "等待 AgentRuntime 回写运行过程。", + "agentApp.apps.runtime.agentRun.thinking": "思考过程", + "agentApp.apps.runtime.agentRun.execution": "执行过程", + "agentApp.apps.runtime.agentRun.output": "成稿流式输出", }; return labels[key] ?? key; }, @@ -92,9 +128,15 @@ interface MountedPage { const mountedPages: MountedPage[] = []; function buildReadyState( - params: { setupResolved?: boolean } = {}, + params: { + setupResolved?: boolean; + manifestPatch?: Partial; + } = {}, ): InstalledAgentAppState { - const manifest = contentFactoryFixture as AppManifest; + const manifest = { + ...(contentFactoryFixture as AppManifest), + ...(params.manifestPatch ?? {}), + } as AppManifest; const loadedAt = "2026-05-15T00:00:00.000Z"; const identity = buildPackageIdentity({ manifest, @@ -377,7 +419,11 @@ describe("AgentAppRuntimePage", () => { }), }), capabilities: expect.objectContaining({ - available: expect.arrayContaining(["lime.agent", "lime.storage"]), + available: expect.arrayContaining([ + "lime.capabilities", + "lime.agent", + "lime.storage", + ]), }), }), }), @@ -467,6 +513,526 @@ describe("AgentAppRuntimePage", () => { ); }); + it("App 可通过 lime.ui 打开、更新并关闭 Host 级 AI 运行面板", async () => { + const container = await renderPage(); + await flush(); + const frame = getRuntimeFrame(container); + const postMessage = vi.spyOn(frame.contentWindow!, "postMessage"); + + await dispatchBridgeMessage( + frame, + "capability:invoke", + { + capability: "lime.ui", + method: "openAgentRun", + input: { + taskId: "agent-app-task-1", + bridgeAction: "content_factory.production", + title: "生成内容批次", + mode: "drawer", + runtimeProcess: { + model: { + provider: "anthropic", + model: "claude-sonnet-4-5", + label: "Claude Sonnet 4.5", + }, + usage: { + inputTokens: 120, + outputTokens: 80, + totalTokens: 200, + }, + cost: { + estimatedTotalCost: 0.0123, + currency: "USD", + }, + skillNames: ["content_factory_writer"], + invokedSkillNames: ["content_factory_writer"], + terminal: false, + timeline: [ + { + kind: "routing", + title: "模型路由", + message: "AgentRuntime 已选择内容生成模型。", + statusText: "decided", + meta: "routing", + }, + { + kind: "skill", + title: "Skill · content_factory_writer", + message: "正在调用内容工厂写作 Skill。", + statusText: "running", + meta: "skill-1", + }, + { + kind: "tool", + title: "Tool · browser_snapshot", + message: "正在读取业务页面上下文。", + statusText: "running", + meta: "tool-1", + }, + { + kind: "execution", + title: "正在规划内容结构", + message: "AgentRuntime 已开始读取项目上下文。", + statusText: "running", + }, + ], + thinkingText: "先确认项目资料,再拆内容主题。", + executionText: "调用内容工厂 Skill。", + streamText: "正在生成第一批文案。", + }, + events: [ + { + eventType: "task:reviewRequested", + requestId: "review-content-batch", + message: "请确认首批内容选题。", + }, + { + eventType: "artifact:created", + artifactRef: ".lime/artifacts/content-batch.json", + payload: { + artifact: { + title: "内容批次 JSON", + }, + }, + }, + { + eventType: "evidence:recorded", + evidenceRef: "evidence:content-batch", + message: "内容批次 evidence 已记录。", + }, + ], + }, + }, + "agent-run-open", + ); + + expect(postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: "host:response", + requestId: "agent-run-open", + payload: expect.objectContaining({ + ok: true, + result: expect.objectContaining({ + opened: true, + surface: "host_agent_run", + mode: "drawer", + taskId: "agent-app-task-1", + }), + }), + }), + "http://127.0.0.1:4199", + ); + expect( + container.querySelector('[data-testid="agent-app-host-agent-run-drawer"]'), + ).toBeNull(); + const dock = container.querySelector( + '[data-testid="agent-app-host-agent-run-dock"]', + ) as HTMLButtonElement; + expect(dock).not.toBeNull(); + expect(container.textContent).toContain("查看运行现场"); + + await act(async () => { + dock.click(); + }); + await flush(); + + expect( + container.querySelector('[data-testid="agent-app-host-agent-run-drawer"]'), + ).not.toBeNull(); + expect( + container.querySelector('[data-testid="agent-run-process-panel"]'), + ).not.toBeNull(); + expect(container.textContent).toContain("生成内容批次"); + expect(container.textContent).toContain("Claude Sonnet 4.5"); + expect(container.textContent).toContain("200"); + expect(container.textContent).toContain("USD 0.0123"); + expect(container.textContent).toContain("content_factory_writer"); + expect(container.textContent).toContain("模型路由"); + expect(container.textContent).toContain("Skill · content_factory_writer"); + expect(container.textContent).toContain("Tool · browser_snapshot"); + expect( + container.querySelector('[data-agent-run-timeline-kind="skill"]'), + ).not.toBeNull(); + expect( + container.querySelector('[data-agent-run-timeline-kind="tool"]'), + ).not.toBeNull(); + expect(container.textContent).toContain("正在规划内容结构"); + expect(container.textContent).toContain("先确认项目资料"); + expect(container.textContent).toContain("待确认"); + expect(container.textContent).toContain("请确认首批内容选题。"); + expect(container.textContent).toContain("review-content-batch"); + expect(container.textContent).toContain("交付物"); + expect(container.textContent).toContain("内容批次 JSON"); + expect(container.textContent).toContain("证据"); + expect(container.textContent).toContain("内容批次 evidence 已记录。"); + + await dispatchBridgeMessage( + frame, + "capability:invoke", + { + capability: "lime.ui", + method: "updateAgentRun", + input: { + taskId: "agent-app-task-1", + runtimeProcess: { + model: { label: "Claude Sonnet 4.5" }, + usage: { inputTokens: 120, outputTokens: 180, totalTokens: 300 }, + terminal: true, + collapsedByDefault: true, + timeline: [ + { + kind: "completed", + title: "内容批次已写回", + message: "Host 保留完整运行过程。", + statusText: "completed", + }, + ], + streamText: "第一批文案已完成。", + }, + }, + }, + "agent-run-update", + ); + + expect(container.textContent).toContain("300"); + expect(container.textContent).toContain("正在规划内容结构"); + expect(container.textContent).toContain("内容批次已写回"); + expect(container.textContent).toContain("运行过程已折叠,点击查看完整现场"); + + await dispatchBridgeMessage( + frame, + "capability:invoke", + { + capability: "lime.ui", + method: "closeAgentRun", + input: { taskId: "agent-app-task-1" }, + }, + "agent-run-close", + ); + + expect(postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: "host:response", + requestId: "agent-run-close", + payload: expect.objectContaining({ + ok: true, + result: expect.objectContaining({ + closed: true, + surface: "host_agent_run", + taskId: "agent-app-task-1", + }), + }), + }), + "http://127.0.0.1:4199", + ); + expect( + container.querySelector('[data-testid="agent-app-host-agent-run-drawer"]'), + ).toBeNull(); + expect( + container.querySelector('[data-testid="agent-app-host-agent-run-dock"]'), + ).toBeNull(); + }); + + it("App 可通过 lime.capabilities.getProfile 发现 Host capability profile", async () => { + const container = await renderPage(); + await flush(); + const frame = getRuntimeFrame(container); + const postMessage = vi.spyOn(frame.contentWindow!, "postMessage"); + + await dispatchBridgeMessage( + frame, + "capability:invoke", + { + capability: "lime.capabilities", + method: "getProfile", + }, + "profile-discovery", + ); + + expect(postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: "host:response", + requestId: "profile-discovery", + payload: expect.objectContaining({ + ok: true, + result: expect.objectContaining({ + appRuntimeVersion: "0.7.0", + standardVersions: { + current: "0.7", + compatible: ["0.5", "0.6", "0.7"], + }, + standards: expect.objectContaining({ + layeredManifest: expect.objectContaining({ + version: "0.5", + enabled: true, + }), + agentRuntime: expect.objectContaining({ + version: "0.6", + enabled: false, + }), + requirementBoundary: expect.objectContaining({ + version: "0.7", + enabled: false, + }), + }), + capabilities: expect.objectContaining({ + "lime.capabilities": expect.objectContaining({ + enabled: true, + implementation: "native", + }), + "lime.agent": expect.objectContaining({ + enabled: true, + }), + "lime.skills": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.memory": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.context": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.search": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.browser": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.documents": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.media": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.mcp": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.terminal": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + "lime.connectors": expect.objectContaining({ + enabled: true, + implementation: "adapter", + }), + }), + }), + }), + }), + "http://127.0.0.1:4199", + ); + expect(postMessage).not.toHaveBeenCalledWith( + expect.objectContaining({ + type: "host:error", + requestId: "profile-discovery", + }), + expect.any(String), + ); + }); + + it("App 可通过 Lime 客户端 profile 读取 v0.6 AgentRuntime 合同", async () => { + const agentRuntime = { + agentTask: { + eventSchema: "lime.agent-task-event.v1", + resultSchema: "lime.agent-task-result.v1", + structuredOutput: { + type: "json_schema", + schemaRef: "./artifacts/content-factory-workspace-patch.schema.json", + }, + approval: { behavior: "host-mediated" }, + sessionPolicy: { modes: ["new", "resume", "continue", "fork"] }, + toolDiscovery: { mode: "on_demand" }, + checkpointScope: { workflowState: true }, + observability: { profileEvents: true }, + }, + }; + const container = await renderPage( + buildReadyState({ + manifestPatch: { + manifestVersion: "0.6.0", + version: "0.6.0", + requires: { + sdk: "@lime/app-sdk@^0.6.0", + capabilities: ["lime.agent", "lime.skills", "lime.usage"], + }, + agentRuntime, + }, + }), + ); + await flush(); + const frame = getRuntimeFrame(container); + const postMessage = vi.spyOn(frame.contentWindow!, "postMessage"); + + await dispatchBridgeMessage( + frame, + "capability:invoke", + { + capability: "lime.capabilities", + method: "getProfile", + }, + "profile-v06-runtime", + ); + + expect(postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: "host:response", + requestId: "profile-v06-runtime", + payload: expect.objectContaining({ + ok: true, + result: expect.objectContaining({ + appRuntimeVersion: "0.7.0", + standardVersions: { + current: "0.7", + compatible: ["0.5", "0.6", "0.7"], + }, + standards: expect.objectContaining({ + layeredManifest: expect.objectContaining({ + version: "0.5", + enabled: true, + layerFiles: expect.arrayContaining(["app.capabilities.yaml"]), + }), + agentRuntime: expect.objectContaining({ + version: "0.6", + enabled: true, + manifestVersion: "0.6", + eventSchema: "lime.agent-task-event.v1", + resultSchema: "lime.agent-task-result.v1", + structuredOutput: true, + approval: true, + sessionPolicy: true, + toolDiscovery: true, + checkpointScope: true, + observability: true, + }), + requirementBoundary: expect.objectContaining({ + version: "0.7", + enabled: false, + }), + }), + agentRuntime, + }), + }), + }), + "http://127.0.0.1:4199", + ); + }); + + it("App 可通过 Lime 客户端 profile 读取 v0.7 需求边界与能力交接", async () => { + const requirements = { + requirements: [ + { + id: "CF-R001", + text: "生成可审核内容草稿", + priority: "mvp", + }, + ], + nonGoals: ["不在 App 包内保存外部凭证"], + }; + const boundary = { + boundaries: [ + { + requirementId: "CF-R001", + planes: { + app: { owns: ["workflow_state"] }, + host: { requires: ["lime.agent", "lime.evidence"] }, + }, + }, + ], + }; + const integrations = [ + { + key: "planning_table", + provider: "cloud.table", + executionPlane: "hybrid", + hostCapability: "lime.connectors", + }, + ]; + const operations = [ + { + key: "write_external_draft", + type: "external_write", + sideEffect: "external_write", + approvalRequired: true, + dryRunRequired: true, + evidenceRequired: true, + autoExecute: false, + }, + ]; + const container = await renderPage( + buildReadyState({ + manifestPatch: { + manifestVersion: "0.7.0", + version: "0.7.0", + requires: { + sdk: "@lime/app-sdk@^0.7.0", + capabilities: ["lime.agent", "lime.connectors", "lime.evidence"], + }, + requirements, + boundary, + integrations, + operations, + }, + }), + ); + await flush(); + const frame = getRuntimeFrame(container); + const postMessage = vi.spyOn(frame.contentWindow!, "postMessage"); + + await dispatchBridgeMessage( + frame, + "capability:invoke", + { + capability: "lime.capabilities", + method: "getProfile", + }, + "profile-v07-handoff", + ); + + expect(postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + type: "host:response", + requestId: "profile-v07-handoff", + payload: expect.objectContaining({ + ok: true, + result: expect.objectContaining({ + appRuntimeVersion: "0.7.0", + standardVersions: { + current: "0.7", + compatible: ["0.5", "0.6", "0.7"], + }, + requirements, + boundary, + integrations, + operations, + standards: expect.objectContaining({ + requirementBoundary: expect.objectContaining({ + version: "0.7", + enabled: true, + manifestVersion: "0.7", + requirementCount: 1, + boundaryCount: 1, + integrationCount: 1, + operationCount: 1, + hostCloudManagedExecution: true, + externalSideEffectsRequireApproval: true, + }), + }), + }), + }), + }), + "http://127.0.0.1:4199", + ); + }); + it("Host Bridge 能在同一 App 作用域内 start / stream / get / cancel / retry Agent task", async () => { const container = await renderPage(); await flush(); @@ -570,7 +1136,10 @@ describe("AgentAppRuntimePage", () => { payload: expect.objectContaining({ ok: true, result: [ - expect.objectContaining({ type: "task:progress", status: "running" }), + expect.objectContaining({ + type: "task:progress", + status: "running", + }), ], }), }), @@ -654,7 +1223,9 @@ describe("AgentAppRuntimePage", () => { sessionId: "agent-app-session-1", turnId: "agent-app-turn-1", }); - expect(runtimeApiMocks.submitAgentAppRuntimeHostResponse).toHaveBeenCalledWith({ + expect( + runtimeApiMocks.submitAgentAppRuntimeHostResponse, + ).toHaveBeenCalledWith({ appId: "content-factory-app", taskId: "agent-app-task-1", runtimeRequest: expect.objectContaining({ diff --git a/src/features/agent-app/ui/AgentAppRuntimePage.tsx b/src/features/agent-app/ui/AgentAppRuntimePage.tsx index 54d9e371c..5251ee8b3 100644 --- a/src/features/agent-app/ui/AgentAppRuntimePage.tsx +++ b/src/features/agent-app/ui/AgentAppRuntimePage.tsx @@ -15,6 +15,7 @@ import { createAgentAppCapabilityDispatcher } from "../runtime/capabilityDispatc import { evaluateAgentAppEntryRuntimeGuard } from "../runtime/entryRuntimeGuard"; import { createAgentAppHostBridge, + type AgentAppHostAgentRunUiRequest, type AgentAppHostBridge, type AgentAppHostBridgeCapabilities, type AgentAppHostBridgeNotifyPayload, @@ -24,22 +25,37 @@ import { buildUiRuntimeCapabilityProfile } from "../runtime/uiRuntimeCapabilityP import type { InstalledAgentAppState, ProjectedEntry } from "../types"; import { buildRuntimePackageLoadForPreview } from "./agentAppsRuntime"; import { resolveInstalledAgentAppDisplayName } from "./agentAppDisplay"; +import { AgentRunHostDrawer, type AgentRunUiState } from "./AgentRunHostDrawer"; const HOST_BRIDGE_DISPATCH_CAPABILITIES = new Set([ + "lime.capabilities", "lime.storage", "lime.artifacts", "lime.evidence", "lime.knowledge", "lime.agent", + "lime.models", + "lime.usage", + "lime.skills", + "lime.memory", + "lime.context", + "lime.search", + "lime.browser", + "lime.documents", + "lime.media", + "lime.mcp", + "lime.terminal", + "lime.connectors", ]); const HOST_BRIDGE_KNOWN_CAPABILITIES = new Set([ ...HOST_BRIDGE_DISPATCH_CAPABILITIES, "lime.workflow", ]); -const RUNTIME_PAGE_FLAGS = buildUiRuntimeCapabilityProfile({ +const RUNTIME_PAGE_PROFILE = buildUiRuntimeCapabilityProfile({ realAdapterEnabled: true, uiRuntimeEnabled: true, -}).featureFlags; +}); +const RUNTIME_PAGE_FLAGS = RUNTIME_PAGE_PROFILE.featureFlags; function isUiEntry(entry: ProjectedEntry): boolean { return ["page", "panel", "settings"].includes(entry.kind); @@ -91,6 +107,7 @@ function resolveHostBridgeCapabilities( item.enabled && HOST_BRIDGE_DISPATCH_CAPABILITIES.has(item.capability), ) .map((item) => item.capability); + available.push("lime.capabilities"); const declared = state.projection.requiredCapabilities.map( (item) => item.capability, ); @@ -106,6 +123,175 @@ function resolveHostBridgeCapabilities( }; } +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function readString(value: unknown): string | null { + return typeof value === "string" && value.trim() ? value.trim() : null; +} + +function readAgentRunTaskId(value: unknown): string | null { + if (!isRecord(value)) { + return null; + } + return ( + readString(value.taskId) ?? + (isRecord(value.task) ? readString(value.task.taskId) : null) ?? + (isRecord(value.snapshot) ? readString(value.snapshot.taskId) : null) + ); +} + +function readAgentRunItemKey(item: unknown, index: number): string { + if (!isRecord(item)) { + return `${index}:${String(item).slice(0, 80)}`; + } + return [ + readString(item.eventId) ?? readString(item.id), + readString(item.eventType) ?? readString(item.type) ?? readString(item.kind), + readString(item.status) ?? readString(item.statusText), + readString(item.message) ?? readString(item.title), + readString(item.occurredAt) ?? readString(item.at) ?? readString(item.createdAt), + ] + .filter(Boolean) + .join("|"); +} + +function mergeAgentRunItems( + previous: unknown, + next: unknown, + limit = 40, +): unknown[] | undefined { + const previousItems = Array.isArray(previous) ? previous : []; + const nextItems = Array.isArray(next) ? next : []; + if (!previousItems.length && !nextItems.length) { + return undefined; + } + const merged: unknown[] = []; + const indexByKey = new Map(); + [...previousItems, ...nextItems].forEach((item, index) => { + const key = readAgentRunItemKey(item, index); + const stableKey = key || `${index}`; + const existingIndex = indexByKey.get(stableKey); + if (existingIndex === undefined) { + indexByKey.set(stableKey, merged.length); + merged.push(item); + return; + } + merged[existingIndex] = item; + }); + return merged.slice(-limit); +} + +function mergeStringArray(previous: unknown, next: unknown): unknown[] | undefined { + const merged = mergeAgentRunItems(previous, next); + return merged?.length ? merged : undefined; +} + +function mergeAgentRunProcess(previous: unknown, next: unknown): unknown { + if (!isRecord(previous)) { + return next ?? previous; + } + if (!isRecord(next)) { + return previous; + } + const merged: Record = { + ...previous, + ...next, + }; + const timeline = mergeAgentRunItems(previous.timeline, next.timeline, 60); + if (timeline) { + merged.timeline = timeline; + } + const skillNames = mergeStringArray(previous.skillNames, next.skillNames); + if (skillNames) { + merged.skillNames = skillNames; + } + const invokedSkillNames = mergeStringArray( + previous.invokedSkillNames, + next.invokedSkillNames, + ); + if (invokedSkillNames) { + merged.invokedSkillNames = invokedSkillNames; + } + for (const key of ["streamText", "thinkingText", "executionText"]) { + if (!readString(merged[key]) && readString(previous[key])) { + merged[key] = previous[key]; + } + } + return merged; +} + +function mergeAgentRunPayload(previous: unknown, next: unknown): unknown { + if (next === null || next === undefined) { + return previous; + } + if (!isRecord(previous) || !isRecord(next)) { + return next; + } + return { + ...previous, + ...next, + events: mergeAgentRunItems(previous.events, next.events, 80), + taskEvents: mergeAgentRunItems(previous.taskEvents, next.taskEvents, 80), + runtimeProcess: mergeAgentRunProcess( + previous.runtimeProcess, + next.runtimeProcess, + ), + process: mergeAgentRunProcess(previous.process, next.process), + }; +} + +function shouldMergeAgentRunUi( + previous: AgentRunUiState | null, + request: AgentAppHostAgentRunUiRequest, +): previous is AgentRunUiState { + if (!previous) { + return false; + } + const previousTaskId = readAgentRunTaskId(previous); + const nextTaskId = readAgentRunTaskId(request); + if (previousTaskId && nextTaskId && previousTaskId !== nextTaskId) { + return false; + } + if ( + previous.bridgeAction && + request.bridgeAction && + previous.bridgeAction !== request.bridgeAction + ) { + return false; + } + return true; +} + +function mergeAgentRunUiState( + previous: AgentRunUiState | null, + request: AgentAppHostAgentRunUiRequest, + now: string, + fallbackMode: AgentRunUiState["mode"], +): AgentRunUiState { + const base = shouldMergeAgentRunUi(previous, request) ? previous : null; + return { + ...base, + ...request, + taskId: request.taskId ?? base?.taskId, + bridgeAction: request.bridgeAction ?? base?.bridgeAction, + title: request.title ?? base?.title, + mode: request.mode ?? base?.mode ?? fallbackMode, + expectedOutput: request.expectedOutput ?? base?.expectedOutput, + runtimeFacts: request.runtimeFacts ?? base?.runtimeFacts, + task: mergeAgentRunPayload(base?.task, request.task), + snapshot: mergeAgentRunPayload(base?.snapshot, request.snapshot), + runtimeProcess: mergeAgentRunProcess( + base?.runtimeProcess, + request.runtimeProcess, + ), + events: mergeAgentRunItems(base?.events, request.events, 100), + openedAt: base?.openedAt ?? now, + updatedAt: now, + }; +} + export function AgentAppRuntimePage({ pageParams, }: { @@ -118,6 +304,8 @@ export function AgentAppRuntimePage({ const [runtime, setRuntime] = useState(null); const [runtimeError, setRuntimeError] = useState(null); const [retryKey, setRetryKey] = useState(0); + const [agentRunUi, setAgentRunUi] = useState(null); + const [agentRunExpanded, setAgentRunExpanded] = useState(false); const frameRef = useRef(null); const hostBridgeRef = useRef(null); @@ -162,6 +350,13 @@ export function AgentAppRuntimePage({ host: capabilityHost, projection: selected.projection, entryKey: activeEntry.key, + profile: RUNTIME_PAGE_PROFILE, + manifestVersion: selected.manifest.manifestVersion, + agentRuntime: selected.manifest.agentRuntime, + requirements: selected.manifest.requirements, + boundary: selected.manifest.boundary, + integrations: selected.manifest.integrations, + operations: selected.manifest.operations, }); }, [activeEntry, capabilityHost, selected]); @@ -247,6 +442,60 @@ export function AgentAppRuntimePage({ [], ); + useEffect(() => { + setAgentRunUi(null); + setAgentRunExpanded(false); + }, [selected?.appId, activeEntry?.key]); + + const openAgentRunUi = useCallback((request: AgentAppHostAgentRunUiRequest) => { + const now = new Date().toISOString(); + const mode = request.mode ?? "drawer"; + setAgentRunUi((previous) => + mergeAgentRunUiState(previous, request, now, mode), + ); + return { + opened: true, + surface: "host_agent_run" as const, + mode, + taskId: request.taskId, + }; + }, []); + + const updateAgentRunUi = useCallback((request: AgentAppHostAgentRunUiRequest) => { + const now = new Date().toISOString(); + setAgentRunUi((previous) => + mergeAgentRunUiState(previous, request, now, previous?.mode ?? "drawer"), + ); + return { + updated: true, + surface: "host_agent_run" as const, + taskId: request.taskId, + }; + }, []); + + const closeAgentRunUi = useCallback( + (request: Pick) => { + setAgentRunUi((previous) => { + if (!previous) { + return null; + } + const sameTask = + !request.taskId || !previous.taskId || previous.taskId === request.taskId; + const sameBridgeAction = + !request.bridgeAction || + !previous.bridgeAction || + previous.bridgeAction === request.bridgeAction; + return sameTask && sameBridgeAction ? null : previous; + }); + return { + closed: true, + surface: "host_agent_run" as const, + taskId: request.taskId, + }; + }, + [], + ); + useEffect(() => { const frame = frameRef.current; if (!frame || !runtime?.entryUrl || !selected || !activeEntry) { @@ -264,6 +513,9 @@ export function AgentAppRuntimePage({ entryRoute: activeEntry.route, entryUrl: runtime.entryUrl, notify: notifyFromApp, + openAgentRunUi, + updateAgentRunUi, + closeAgentRunUi, capabilities: hostBridgeCapabilities, dispatchCapability, }); @@ -281,12 +533,15 @@ export function AgentAppRuntimePage({ } }, [ activeEntry, + closeAgentRunUi, dispatchCapability, displayName, hostBridgeCapabilities, notifyFromApp, + openAgentRunUi, runtime?.entryUrl, selected, + updateAgentRunUi, ]); const handleFrameLoad = useCallback(() => { @@ -346,7 +601,7 @@ export function AgentAppRuntimePage({ return (