Release v1.66.0

This commit is contained in:
coso
2026-06-11 09:21:21 +08:00
parent 1bd2c42bf2
commit 5883c21f2d
456 changed files with 21369 additions and 47473 deletions
+6
View File
@@ -123,6 +123,12 @@ internal/roadmap/i18n/**
!internal/roadmap/agentapp/
internal/roadmap/agentapp/**
!internal/roadmap/agentapp/*.md
!internal/roadmap/agentworkbench/
internal/roadmap/agentworkbench/**
!internal/roadmap/agentworkbench/*.md
!internal/roadmap/subagents/
internal/roadmap/subagents/**
!internal/roadmap/subagents/*.md
!internal/roadmap/zuanjia/
internal/roadmap/zuanjia/**
!internal/roadmap/zuanjia/*.md
+23 -21
View File
@@ -1,42 +1,44 @@
## Lime v1.65.0
## Lime v1.66.0
<sub>The Simplified Chinese release notes are the primary version. This English page is a companion for international readers.</sub>
### New Features
- `lime media video generate` now uses the current media runtime to create and execute video-generation tasks, resolve service endpoint/API key from local config or environment variables, and persist progress, error, and artifact state.
- `@limecloud/agent-runtime-projection` now exposes dedicated builders for artifact, context, conversation, diagnostic, hydration, lifecycle, permission, plan approval, queue, thread item, and tool events.
- Agent Chat projections continue to consume the shared projection package for runtime state, queue, permissions, history hydration, tool timeline, thread items, and context-event read model semantics.
- Agent App runtime now connects to the current App Server client and capability APIs, allowing standalone Agent Apps to reuse the JSON-RPC client, capability host, and runtime projection path.
- Agent Runtime standard packages now include App Server facts, fixture replay, subagents, refs, and validation support for shared runtime, projection, and UI consumption.
- App Server workspace protocol now exposes project-summary reads, with matching Rust client, npm `app-server-client`, and frontend project API updates.
- The Agent input bar can read and surface project context so workspace project summaries can participate in the main chat orchestration path.
### Fixes
- Fixed the video-generation skill recommendation so it points at the current `lime media video generate --prompt "..." --aspect-ratio 9:16` entrypoint.
- Fixed governance wording that could still treat old `lime-rs/src/**` files as current Rust owners; restoring old Tauri wrappers, stubs, or legacy facades is now explicitly guarded as backflow.
- Removed duplicate voice settings dependencies on ASR credentials and shortcut runtime state, reducing old command-surface coupling and making the current voice model install state the default source for model status.
- Fixed Agent UI projection summary and subagents read-model naming so old Team Workbench terminology no longer leaks into the current model.
- Fixed Agent App runtime page and projection bridge wiring for the current capability host / client APIs, reducing drift between standalone apps and the desktop host.
- Fixed several state-sync issues around the input bar, tool display, workspace send runtime, and thread grouping.
- Fixed DevBridge command policy and legacy surface catalog classification for retired command surfaces.
### Improvements And Refactors
- Physically removed the old orphaned `lime-rs/src/**` directory, including old Tauri command, service, dev_bridge, and runner implementations; Rust backend ownership is now centered on `lime-rs/crates/**`.
- Moved agent tools catalog, execution, and inventory into `lime-rs/crates/agent`; the old path now exists only as deletion history, not as a runtime owner.
- Split video-generation CLI logic into `lime-rs/crates/lime-cli/src/video.rs` and moved video task execution into `lime-rs/crates/media-runtime/src/video_worker.rs`.
- Reduced duplicated Agent Chat frontend projection state machines by moving reusable event semantics into the npm projection package.
- Simplified the voice and hotkey settings boundary: the voice page focuses on models, instructions, and preferences, while the hotkey page owns global shortcut configuration and validation.
- Converged the Agent Chat workbench path from old Team Workspace components, selectors, canvas runtime, and suggestion helpers onto the current subagents / workbench presentation, deleting a large amount of retired team-workspace UI surface.
- Split `AppSidebar` into account, appearance, invite, search, session, navigation-target, and style modules, reducing single-file complexity.
- Removed Companion API, settings card, provider overview, desktop mock, and sidebar-entry remnants from the current settings and provider surfaces.
- Continued moving input-bar project context, team preference, project storage, and workspace selection logic into hooks and focused helpers.
- Expanded the Agent Runtime / Agent UI npm packages with standard contracts, fixtures, projection helpers, runtime facts, and UI exports to reduce duplicated GUI and SDK implementation.
### Tests And Quality
- Added Agent Runtime projection unit coverage for artifact, context, diagnostic, hydration, lifecycle, permission, routing, and turn context events, and expanded the main projection regression suite.
- Updated App Server client contracts, Harness contracts, Rust current-boundary guards, legacy tool permission guards, and Electron current rules guards to prevent old paths from becoming fact sources again.
- Expanded regressions for the input bar, Markdown rendering, Agent Chat home surface, voice settings, hotkey settings, media tasks, gallery materials, session images, and video diagnostics.
- Updated the root app, Rust workspace, CLI npm package, Agent App runtime package, App Server client package, Agent Runtime client dependency, and lockfiles to `1.65.0`.
- Expanded App Server protocol catalog, workspace project API, npm `app-server-client`, Agent Runtime client, projection, UI contracts, and fixture replay regressions.
- Updated AppSidebar, Agent Chat input bar, workspace scene, workspace send, settings v2, Agent App runtime page, and i18n resource tests.
- Updated Electron SDK fixture smoke, tool-surface smoke, command-contract checks, quality-task planning, and i18n readiness reporting.
- Updated the root app, Rust workspace, CLI npm package, Agent App runtime package, App Server client package, Agent Runtime client dependency, and lockfiles to `1.66.0`.
### Documentation
- Updated AGENTS, quality workflow, governance, command-boundary, and parallel-collaboration docs to record the June 10, 2026 `lime-rs/src/**` deletion and the directory-level dead-surface decision rule.
- Updated production command current migration, Tauri wrapper inventory / cleanup queue, tech-debt tracking, and the App Server frontend integration matrix.
- Updated Agent Runtime projection and Lime CLI npm package documentation with the current projection modules and video-generation entrypoint.
- Added Agent Workbench and Subagents roadmap entries, including acceptance, iteration plan, parallel workstreams, and task board artifacts.
- Updated Agent Runtime, Agent UI standard gap, completion audit, implementation plan, test cases, and adjacent protocol documentation.
- Updated quality workflow, command-boundary, Playwright E2E, protocol standards map, and tech-debt tracking docs for the current workbench / subagents / App Server boundaries.
### Other
- This release continues to center release facts on App Server JSON-RPC, Electron Desktop Host, current clients, `lime-rs/crates/**`, and machine-readable guards while blocking old Tauri wrappers and renderer mocks from returning to GUI production paths.
- This release keeps release facts centered on App Server JSON-RPC, Electron Desktop Host, current npm clients, `lime-rs/crates/**`, and machine-readable guards while preventing old Team Workspace, Companion, and legacy command surfaces from flowing back into the product path.
**Full changes**: `v1.64.0` -> `v1.65.0`
**Full changes**: `v1.65.0` -> `v1.66.0`
+23 -21
View File
@@ -1,34 +1,36 @@
## Lime v1.65.0
## Lime v1.66.0
### 新功能
- `lime media video generate` 接入 current media runtime,可直接创建并执行视频生成任务,按本地配置或环境变量解析服务端点与 API Key,并写回进度、错误和产物状态
- `@limecloud/agent-runtime-projection` 新增 artifact、context、conversation、diagnostic、hydration、lifecycle、permission、plan approval、queue、thread item 与 tool event 等标准投影构建模块
- Agent Chat 投影继续复用共享 projection 包,统一运行态、队列、权限、历史 hydrate、工具轨迹、线程条目与上下文事件的 read model 语义
- Agent App runtime 增加 App Server current client / capability API 接入,独立 Agent App 可以复用当前 JSON-RPC 客户端、能力宿主与运行态投影
- Agent Runtime 标准包补齐 App Server facts、fixture replay、subagents、refs 与 validation 支持,方便外部运行态、投影层和 UI 组件消费同一组事实
- App Server workspace 协议新增项目摘要读取能力,并同步到 Rust client、npm `app-server-client` 与前端项目 API
- Agent 输入框增加项目上下文读取与展示能力,支持把当前工作区项目摘要带入主对话编排。
### 修复
- 修复视频生成 skill 的推荐命令,优先指向 `lime media video generate --prompt "..." --aspect-ratio 9:16` current 入口
- 修复`lime-rs/src/**` 仍可能被误判为 current Rust owner 的治理口径,明确恢复旧 Tauri wrapper、stub 或 legacy facade 都属于回流
- 修复语音设置页对 ASR credential 与快捷键运行态的重复依赖,降低旧命令面影响,默认语音模型状态以 current voice model install state 为准
- 修复 Agent UI projection summary 与 subagents read model 的命名和汇总口径,减少 Team Workbench 旧语义残留
- 修复 Agent App runtime 页面和投影桥接对 current capability host / client API 的接线,降低独立 App 与桌面宿主之间的协议漂移
- 修复输入框、工具展示、workspace send runtime 与 thread grouping 的若干状态同步问题
- 修复 DevBridge command policy 与 legacy surface catalog 对旧命令面的分类,避免已退场路径继续被误判为 current。
### 优化与重构
- 物理删除旧 `lime-rs/src/**` 孤儿目录及旧 Tauri command / service / dev_bridge / runner 实现,Rust 后端事实源收敛到 `lime-rs/crates/**`
- 将 agent tools catalog、execution 与 inventory 迁入 `lime-rs/crates/agent`,旧路径只保留为删除记录,不再作为运行时 owner
- 将视频生成 CLI 逻辑拆到 `lime-rs/crates/lime-cli/src/video.rs`,并把视频任务执行能力沉入 `lime-rs/crates/media-runtime/src/video_worker.rs`
- 收缩 Agent Chat 前端投影中的重复状态机,把可复用事件规范沉淀到 npm projection 包,减少 React 组件侧重复分支
- 简化语音与热键设置边界:语音页聚焦模型、指令和偏好,快捷键页承接全局热键配置与校验
- Agent Chat 工作台主线从旧 Team Workspace 组件、selector、canvas runtime 与 suggestion 工具收敛到 subagents / workbench current 表达,删除大批旧 team workspace UI 面
- `AppSidebar` 拆分为 account、appearance、invite、search、session、navigation target 与样式等子模块,显著降低单文件复杂度
- 下线 Companion 相关 API、设置卡片、provider overview、desktop mock 与侧边入口残留,减少旧 companion 能力对当前设置页和 provider 面的干扰
- 输入框项目上下文、team preference、project storage 与 workspace selection 逻辑继续向 hook / helper 分层收敛
- Agent Runtime / Agent UI npm 包继续补齐标准 contracts、fixtures、projection、runtime facts 与 UI exports,减少 GUI 与 SDK 的重复实现
### 测试与质量
- 新增 Agent Runtime projection 的 artifact、context、diagnostic、hydration、lifecycle、permission、routing 与 turn context 单测,并扩展主 projection 回归。
- 更新 App Server client contract、Harness contract、Rust current boundary、legacy tool permission guard 与 Electron current rules guard,防止旧路径重新成为事实源
- 扩展输入框、Markdown、Agent Chat home surface、语音设置、热键设置、媒体任务、图库素材、session images 与视频诊断相关回归
- 根应用、Rust workspace、CLI npm package、Agent App runtime package、App Server client package、Agent Runtime client 依赖与锁文件版本统一更新到 `1.65.0`
- 扩展 App Server protocol catalog、workspace project API、npm `app-server-client`、Agent Runtime client、projection、UI contracts 与 fixture replay 回归。
- 更新 AppSidebar、Agent Chat inputbar、workspace scene、workspace send、settings v2、Agent App runtime page 与 i18n 资源相关测试
- 更新 Electron SDK fixture smoke、tool surface smoke、command contract 检查、质量任务规划与 i18n readiness 报告
- 根应用、Rust workspace、CLI npm package、Agent App runtime package、App Server client package、Agent Runtime client 依赖与锁文件版本统一更新到 `1.66.0`
### 文档
- 更新 AGENTS、工程质量、治理、命令边界与并行协作文档,记录 `lime-rs/src/**` 已于 2026-06-10 删除以及目录级 dead 判定口径
- 更新 production command current migration、Tauri wrapper inventory / cleanup queue、tech debt tracker 与 App Server frontend integration matrix
- 更新 Agent Runtime projection 与 Lime CLI npm package 文档,补充 current 投影模块与视频生成入口说明
- 新增 Agent Workbench 与 Subagents 路线图入口,补充 acceptance、iteration plan、parallel workstreams 与 task board
- 更新 Agent Runtime、Agent UI 标准落差、completion audit、implementation plan、test cases 与 adjacent protocols 文档
- 更新工程质量、命令边界、Playwright E2E、协议标准地图与技术债追踪文档,记录当前 workbench / subagents / App Server 主线边界
### 其他
- 本版继续 App Server JSON-RPC、Electron Desktop Host、current clients、`lime-rs/crates/**` 与机器可读守卫作为发布事实源,阻断旧 Tauri wrapper 和 renderer mock 对 GUI 主路径的回流。
- 本版继续把发布事实源收敛到 App Server JSON-RPC、Electron Desktop Host、current npm clients、`lime-rs/crates/**` 与机器可读守卫,避免旧 Team Workspace、Companion 和 legacy command 面回流。
**完整变更**: `v1.64.0` -> `v1.65.0`
**完整变更**: `v1.65.0` -> `v1.66.0`
+3 -3
View File
@@ -4,7 +4,7 @@ import {
encodeMessage,
type JsonRpcMessage,
type JsonRpcRequest,
} from "app-server-client";
} from "@limecloud/app-server-client";
const {
fakeConnection,
@@ -118,8 +118,8 @@ Object.defineProperty(process, "resourcesPath", {
value: process.cwd(),
});
vi.mock("app-server-client", async (importOriginal) => {
const actual = await importOriginal<typeof import("app-server-client")>();
vi.mock("@limecloud/app-server-client", async (importOriginal) => {
const actual = await importOriginal<typeof import("@limecloud/app-server-client")>();
return {
...actual,
AppServerSidecarLifecycle: FakeAppServerSidecarLifecycle,
+1 -1
View File
@@ -20,7 +20,7 @@ import {
type JsonRpcMessage,
type RequestId,
type SidecarLaunchConfig,
} from "app-server-client";
} from "@limecloud/app-server-client";
import { app } from "./electronRuntime";
import { readFileSync } from "node:fs";
import path from "node:path";
+1 -1
View File
@@ -14,7 +14,7 @@ import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { AppServerRequestError } from "app-server-client";
import { AppServerRequestError } from "@limecloud/app-server-client";
import { ElectronHostCommands } from "./hostCommands";
import type { ElectronAppServerHost } from "./appServerHost";
+33 -2
View File
@@ -29,6 +29,7 @@ import {
METHOD_WORKSPACE_BY_PATH_READ,
METHOD_WORKSPACE_DEFAULT_ENSURE,
METHOD_WORKSPACE_DEFAULT_READ,
METHOD_WORKSPACE_ENSURE,
METHOD_WORKSPACE_ENSURE_READY,
METHOD_WORKSPACE_LIST,
METHOD_WORKSPACE_PROJECTS_ROOT_READ,
@@ -56,6 +57,7 @@ import {
type ModelListResponse,
type ModelProviderListResponse,
type ProjectMemoryReadResponse,
type WorkspaceEnsureProjectResponse,
type SkillListResponse,
type WorkspaceEnsureReadyResponse,
type WorkspaceListResponse,
@@ -63,7 +65,7 @@ import {
type WorkspaceProjectsRootReadResponse,
type WorkspaceReadResponse,
type WorkspaceSkillBindingsListResponse,
} from "app-server-client";
} from "@limecloud/app-server-client";
import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import {
@@ -367,6 +369,8 @@ export class ElectronHostCommands {
return await this.#readWorkspace(args);
case "workspace_get_by_path":
return await this.#readWorkspaceByPath(args);
case "workspace_ensure":
return await this.#ensureWorkspace(args);
case "workspace_get_projects_root":
return await this.#readWorkspaceProjectsRoot();
case "workspace_resolve_project_path":
@@ -1732,6 +1736,34 @@ export class ElectronHostCommands {
return response.rootPath;
}
async #ensureWorkspace(args: HostArgs): Promise<unknown> {
const request = readRequest(args);
const name =
readString(request, "name") ?? readString(args, "name") ?? "untitled";
const rootPath =
readString(request, "rootPath") ??
readString(request, "root_path") ??
readString(args, "rootPath") ??
readString(args, "root_path");
if (!rootPath) {
throw new Error("workspace_ensure requires rootPath");
}
const workspaceType =
readString(request, "workspaceType") ??
readString(request, "workspace_type") ??
readString(args, "workspaceType") ??
readString(args, "workspace_type");
const response = await this.#appServerRequest<WorkspaceEnsureProjectResponse>(
METHOD_WORKSPACE_ENSURE,
{
name,
rootPath,
...(workspaceType ? { workspaceType } : {}),
},
);
return response.workspace;
}
async #ensureDefaultWorkspaceReady(): Promise<unknown | null> {
const workspace = await this.#ensureDefaultWorkspace();
const id = readString(workspace, "id");
@@ -4197,7 +4229,6 @@ function buildDefaultConfig(): Record<string, unknown> {
workspace_preferences: {
schema_version: 3,
media_defaults: {},
companion_defaults: {},
service_models: {},
},
navigation: { schema_version: 3, enabled_items: [] },
+2
View File
@@ -63,6 +63,7 @@ describe("electron/ipcChannels", () => {
expect(isElectronHostCommand("read_file_preview_cmd")).toBe(false);
expect(isElectronHostCommand("list_executable_skills")).toBe(false);
expect(isElectronHostCommand("workspace_list")).toBe(true);
expect(isElectronHostCommand("workspace_ensure")).toBe(true);
expect(isElectronHostCommand("workspace_get_by_path")).toBe(true);
expect(isElectronHostCommand("workspace_set_default")).toBe(false);
expect(isElectronHostCommand("get_config")).toBe(true);
@@ -198,6 +199,7 @@ describe("electron/ipcChannels", () => {
"get_default_provider",
"get_local_skills_for_app",
"project_memory_get",
"workspace_ensure",
"workspace_ensure_default_ready",
"workspace_ensure_ready",
"workspace_get",
+2
View File
@@ -74,6 +74,7 @@ export const ELECTRON_HOST_COMMANDS = [
"voice_models_download",
"voice_models_get_install_state",
"voice_models_list_catalog",
"workspace_ensure",
"workspace_ensure_default_ready",
"workspace_ensure_ready",
"workspace_get",
@@ -131,6 +132,7 @@ export const ELECTRON_APP_SERVER_TRUTH_BRIDGE_COMMANDS = [
"get_default_provider",
"get_local_skills_for_app",
"project_memory_get",
"workspace_ensure",
"workspace_ensure_default_ready",
"workspace_ensure_ready",
"workspace_get",
+1 -1
View File
@@ -35,7 +35,7 @@ import {
PROTOCOL_VERSION,
SERVER_NAME,
type InitializeResponse,
} from "app-server-client";
} from "@limecloud/app-server-client";
import {
app,
BrowserWindow,
+1
View File
@@ -23,6 +23,7 @@
- `overview.md` - 项目架构总览与模块分层
- `agent-protocol-standards-map.md` - Agent 公共标准、Lime 开发协议、友链与未来拆分候选地图
- `agent-ui-runtime-standard.md` - Agent UI / Runtime 四包职责、宿主接入、主聊天 projection 迁移分类与守卫要求
- Agent UI / Runtime 当前宿主链路固定为 `AgentRuntimeClient -> projectAgentUiState -> AgentUiProjectionView`;任何第二宿主复用都必须回到 `agent-ui-runtime-standard.md`
- `query-loop.md` - 运行时 Query Loop current 主链、提交入口与执行边界
- `prompt-foundation.md` - 基础 Prompt current 主链、system prompt 组装顺序与 current/compat 分类
- `task-agent-taxonomy.md` - Task / Agent / Coordinator current taxonomy、current/compat 分类与协调边界
@@ -65,6 +65,8 @@
| Persistence Map | `persistence-map.md` | FileArtifact、sidecar、version、checkpoint、snapshot refs | Artifact 标准全文 |
| Quality Workflow | `quality-workflow.md` | 本地校验、GUI smoke、contracts、风险分类 | 产品标准定义 |
当前 Agent UI / Runtime 的宿主复用验收链路固定为 `AgentRuntimeClient -> projectAgentUiState -> AgentUiProjectionView`;公共标准只定义可移植语义,Lime 实现必须在 `agent-ui-runtime-standard.md` 中继续收敛到这条链路。
## 未来可拆标准候选
这些方向已经在 Lime 中出现稳定压力,但是否独立成公共标准,取决于是否能脱离 Lime 私有实现并服务其他 Agent 产品。
+37 -12
View File
@@ -23,12 +23,12 @@ App Server JSON-RPC current methods
| 包 | 分类 | 拥有的事实 | 禁止承接 |
| --- | --- | --- | --- |
| `@limecloud/agent-runtime-client` | current runtime facade | `agentSession/turn/start``agentSession/turn/cancel``agentSession/action/respond``agentSession/read``evidence/export``agentSession/event` 的 TypeScript facade | 新 JSON-RPC method、Electron IPC、legacy command、mock fallback、独立 `readTask` 协议 |
| `@limecloud/agent-ui-contracts` | current 类型事实源 | execution event、Agent UI adapter event、message parts、process timeline、execution graph、projection state、projector interface | 投影逻辑、React 组件、JSON-RPC client、session store、业务对象 |
| `@limecloud/agent-runtime-projection` | current headless projection | event store index、scope selector、latest selector、summary selector、read model、`projectAgentUiState``createAgentUiProjector` | React、DOM、i18n hook、App Server client、Electron bridge、mock、宿主业务路径 |
| `@limecloud/agent-runtime-ui` | current React primitives | `AgentUiProjectionState` 的标准渲染组件、message parts、timeline、execution graph、tool/action/artifact/evidence 事实展示、action intent callbacks | JSON-RPC 订阅、session 持久化、业务路由、全局主题、Prompt/知识库/素材/审核业务壳 |
| `@limecloud/agent-runtime-client` | current runtime facade | `agentSession/turn/start``agentSession/turn/cancel``agentSession/action/respond``agentSession/read``evidence/export``agentSession/event` 的 TypeScript facadebrowser-safe 子路径 `@limecloud/agent-runtime-client/sessionGateway` 提供 `createAgentRuntimeClientFromSessionGateway(...)`,把已有 App Server session gateway 适配为标准 lifecycle client | 新 JSON-RPC method、Electron IPC、legacy command、mock fallback、独立 `readTask` 协议、宿主业务 session 创建 |
| `@limecloud/agent-ui-contracts` | current 类型事实源 | execution event、Agent UI adapter event、message parts、process timeline、execution graph、Subagents、projection state、projector interface | 投影逻辑、React 组件、JSON-RPC client、session store、业务对象 |
| `@limecloud/agent-runtime-projection` | current headless projection | event store index、scope selector、latest selector、summary selector、read model、`projectAgentUiState``createAgentUiProjector``buildAgentUiSubagentsModel` | React、DOM、i18n hook、App Server client、Electron bridge、mock、宿主业务路径 |
| `@limecloud/agent-runtime-ui` | current React primitives | `AgentUiProjectionState` 的标准渲染组件、message parts、timeline、execution graph、Subagents、tool/action/artifact/evidence 事实展示、action intent callbacks | JSON-RPC 订阅、session 持久化、业务路由、全局主题、Prompt/知识库/素材/审核业务壳 |
`app-server-client` 仍是底层 App Server JSON-RPC 与 sidecar lifecycle client`agent-runtime-client` 是对外标准 facade,不是第二套协议。
`@limecloud/app-server-client` 仍是底层 App Server JSON-RPC 与 sidecar lifecycle client`@limecloud/agent-runtime-client` 是对外标准 facade,不是第二套协议。
## Canonical 包命名决策
@@ -75,6 +75,7 @@ src/refs.ts -> artifact refs 等跨宿主引用提取
src/routing.ts -> routing run.status / decision payload projection helpers
src/runtimeFacts.ts -> runtime entity/status/phase/topology 标准事实解释
src/summary.ts -> host-neutral summary / surface / lane selectors
src/subagents.ts -> AgentUiSubagentsModel 标准模型 selector
src/subagentStatusEvents.ts -> subagent_status_changed team / handoff / worker notification projection helpers
src/threadItems.ts -> thread item -> AgentUiProjectionEvent builders / action item builders / subagent activity builders / task owner metadata parser
src/toolEvents.ts -> tool_start / tool_end / tool_progress / tool delta -> AgentUiProjectionEvent builders
@@ -93,7 +94,9 @@ src/labels.ts -> 默认 label、status 和 meta formatter
src/messages.tsx -> AgentTimeline / UIMessagePartsView
src/processTimeline.tsx -> ProcessTimelineView
src/executionGraph.tsx -> ExecutionGraphView
src/refs.tsx -> ArtifactRefList / EvidenceRefList / AgentUiRefList
src/runtimeFacts.tsx -> RuntimeFactsPanel / RuntimeFactCard / action/tool lists
src/subagents.tsx -> SubagentsView / SubagentThreadList / SubagentDelegationList / SubagentActivityList
src/projectionView.tsx -> AgentUiProjectionView 标准组合入口
src/index.ts -> barrel exports only
```
@@ -135,7 +138,7 @@ src/index.ts -> barrel exports only
- scope 匹配、latest event index、artifact / evidence / action lookup 的本地重写。
- action / task / artifact / evidence / diagnostics 分组的本地重写。
- Team Workbench surface / lane 聚合的本地重写。
- Subagents 线程、委派调用、活动摘要、隔离摘要聚合的本地重写。
- 从 UI 状态反推 runtime fact 的逻辑。
- mock fallback 或 legacy command fallback。
@@ -158,13 +161,13 @@ src/index.ts -> barrel exports only
| `routingProjection.ts` | compat adapter | 只允许把主聊天 routing decision / limit state / limit event 字段映射到标准 `buildAgentUiRoutingStatusEvent`run.status 的 owner/scope/phase/surface/persistence 与 routing / limit payload 已由标准 helper 接管 | 周边投影模块改为直接消费标准 routing helper 后删除 |
| `runtimeLifecycleProjection.ts` 中 lifecycle / model / task profile 构建 | compat adapter | 只允许把主聊天 `thread_started` / `turn_started` / `turn_completed` / `turn_failed` / `runtime_status` / `model_change` / `task_profile_resolved` 字段映射到标准 `buildAgentUiThreadStartedEvent` / `buildAgentUiRunStartedEvent` / `buildAgentUiRunFinishedEvent` / `buildAgentUiRunFailedEvent` / `buildAgentUiRuntimeStatusEvent` / `buildAgentUiRuntimeTeamChangedEvent` / `buildAgentUiModelChangeEvent` / `buildAgentUiTaskProfileResolvedEvent` | 周边投影模块改为直接消费标准 lifecycle helper 后删除通用兼容构建 |
| `subagentStatusProjection.ts` | compat adapter | 只允许把主聊天 `runtime_status` 字段映射到标准 `buildAgentUiRuntimeTeamChangedEvent`,并把 `subagent_status_changed` 字段映射到标准 `buildAgentUiSubagentStatusChangedEvents`runtime status 来源 `team.changed` 与 subagent status 来源 team / handoff / transcript / worker notification 复合事件的 owner/scope/phase/surface/persistence/payload 已由标准 helper 接管 | 周边投影模块改为直接消费标准 subagent status helper 后删除 |
| `threadItemProjection.ts` 中 thread item 主事件构建 | compat adapter | 只允许把主聊天 `AgentThreadItem` 映射到标准 `buildAgentUiThreadItemEvent` / `buildAgentUiThreadItemBase`,并通过 `extractAgentUiTaskOwnerChangeProjection` 读取 TaskUpdate owner metadata;审批、用户输入、subagent activity、worker notification 和计划审批 metadata 已由标准 helper 接管Team Workbench 附加事件暂留本地 adapter | 周边投影模块改为直接消费标准 thread item helper 后删除通用兼容构建 |
| `threadItemProjection.ts` 中 thread item 主事件构建 | compat adapter | 只允许把主聊天 `AgentThreadItem` 映射到标准 `buildAgentUiThreadItemEvent` / `buildAgentUiThreadItemBase`,并通过 `extractAgentUiTaskOwnerChangeProjection` 读取 TaskUpdate owner metadata;审批、用户输入、subagent activity、worker notification 和计划审批 metadata 已由标准 helper 接管;旧协作视图附加事件只能作为主聊天 presentation residual 暂留 | 周边投影模块改为直接消费标准 thread item helper 后删除通用兼容构建 |
| `toolEventProjection.ts` | compat adapter | 只允许把主聊天 `tool_start` / `tool_end` / `tool_progress` / `tool_output_delta` / `tool_input_delta` 字段映射到标准 `buildAgentUiTool*` helpertool lifecycle、artifact refs、diagnostic keys 和 plan approval 附加 action 已由标准 helper 接管 | 周边投影模块改为直接消费标准 tool helper 后删除 |
| `agentUiEventProjection.ts` 中从主聊天事件构建 `AgentUiProjectionEvent` 的代码 | compat adapter | 仍依赖主聊天 `agentProtocol` 和历史事件形状,是迁移输入适配层;通用 envelope / sequence 规则已迁入标准 projection 包 | host-neutral 构建规则继续分批迁入标准 projection 包或 App Server event adapter 后收缩 |
| `conversationProjectionStore.ts` | current host store adapter | 只保留 external store、订阅、stream diagnostics 和兼容导出名,selector 委托标准包 | 标准 store adapter 被两个宿主复用后继续收缩 |
| `agentUiProjectionSummary.ts` 中本地化 label / formatter | current presentation adapter | 只负责中文/本地化展示和主聊天 UI 细节 | 标准 UI package 提供可配置 formatter 后再迁 |
| `agentUiProjectionSummary.ts` 中 selector / event type set / surface lane | moved current | 已迁到 `@limecloud/agent-runtime-projection` | 守卫阻止回流 |
| `agentUiTeamWorkbenchViewModel.ts` | current host view model | 负责主聊天 Team Workbench 的 item / chip / action / target view model | host-neutral selector 继续下沉后保留 presentation 层 |
| `agentUiSubagentsViewModel.ts` | compat presentation residual | 只允许消费标准 projection / `state.subagents` / summary selector 后生成主聊天本地 item / chip / action / target 展示模型;不得作为 SDK 类型、组件、state 字段或新宿主 API;旧协作命名文件已退出 projection adapter 命名 | host-neutral selector 下沉到 `@limecloud/agent-runtime-projection` 后,只保留宿主业务 action route;剩余 workspace / 产品测试参数名随产品侧迁移删除 |
| `*.js` / `*.d.ts` 旧生成物残留 | deprecated | 只能做薄转导或待删除,不得保留旧实现 | 删除需要独立确认并同步守卫 |
任何新增 Agent UI 标准 selector 必须先进入 `@limecloud/agent-runtime-projection`;主聊天目录只能做适配、展示和业务交互。
@@ -174,6 +177,8 @@ src/index.ts -> barrel exports only
Agent App Runtime 是当前第二个真实宿主基线:
```text
AgentRuntimeClient -> projectAgentUiState -> AgentUiProjectionView
Agent App host run state
-> buildAgentAppStandardRuntimeEvents(...)
-> projectAgentUiState(...)
@@ -187,6 +192,14 @@ Agent App 可以继续保留自己的 summary、运行抽屉、业务对象 pres
标准 UI 必须支持多 action controls。`AgentRuntimeEventProjection.action` 保持兼容单按钮读取,`AgentRuntimeEventProjection.actions` 承接 approve / reject / answer / retry / stop 等多按钮 intent`agent-runtime-ui` 只能渲染这些 intent 并回调宿主,不得自行解释业务结果。
Agent App 第二宿主的 runtime client 接入以 `src/features/agent-app/runtime/agentRuntimeClientApi.ts` 为 current adapter。`AgentRuntimeCapabilityHost` 可以直接注入 `AgentRuntimeClient`,由该 adapter 把 `lime.agent.startTask/getTask/cancelTask/submitHostResponse` 分别投影到 `startTurn/readThread/cancelTurn/respondAction``startTask` 必须携带已经存在的 `sessionId`,缺失时 fail closed,不在前端伪造 session 或独立 `readTask` 协议。adapter 负责写入 `runtimeOptions.hostOptions.asterChatRequest`,并保留 `turn_config` 镜像,保证后端 host 与 App Server backend 都能读取同一份 provider / 权限 / sandbox / system prompt / metadata 配置。`readThread` 返回的 `detail.thread_read` / `detail.threadRead` 是 Agent App host replay artifact、tool call 和 evidence 的 read model 事实源。
Agent App 默认运行页必须通过 `src/features/agent-app/runtime/agentRuntimeAppServerClient.ts` 先调用 App Server current `agentSession/start` 创建或绑定 session,再把该 sessionId 交给 `agentRuntimeClientApi.ts` 进入 `startTurn/readThread/cancelTurn/respondAction``readSession -> readThread` 的通用 lifecycle 适配必须复用 `@limecloud/agent-runtime-client/sessionGateway``createAgentRuntimeClientFromSessionGateway(...)`Agent App feature 只保留 `agent_app.task` business object session resolver 与默认页面装配,不得重新实现私有 session gateway。renderer 运行时代码不得为了取 session gateway 适配器而从 `@limecloud/agent-runtime-client` 根入口导入运行时值,否则会把 Node 侧 App Server client 打进前端包。`src/features/agent-app/ui/AgentAppRuntimePage.tsx` 默认注入 `createDefaultAgentAppRuntimeHostOptions()`;这条默认宿主链路不得在标准 client 失败后回退 `agent_app_runtime_*`、renderer mock 或 desktop-host default mock,而应 fail closed。
`AgentRuntimeCapabilityHost` 默认不再隐式回退 `agent_app_runtime_*` compat facade:没有注入标准 `AgentRuntimeClient`,也没有显式传入 compat `api` 时必须 fail closed。`src/lib/api/agentAppRuntime.ts``agent_app_runtime_*` 只保留为显式兼容入口和迁移期守卫,服务尚未迁完的既有桌面路径;新增第二宿主能力必须优先走 `AgentRuntimeClient` adapter。compat facade 只能委托、适配和退场,不得复制运行时提交、read model 拼装、tool replay 或 mock fallback。
DevBridge policy 中 `agent_app_runtime_*` 必须归类为 no-mock compat:它们可以 fail closed、走真实 Electron Desktop Host / App Server current 通道或作为迁移期显式兼容入口,但不得继续放在 `bridgeTruthCommands` 里充当后续演进事实源,也不得进入 `mockPriorityCommands`、desktop-host default mock 或 renderer mock fallback。
## App Server 与 Runtime 配合
Agent Runtime 后端事实源是 App Server JSON-RPC current 主链:
@@ -246,12 +259,24 @@ Electron 只作为 Desktop Host bridge 和 sidecar lifecycle host。新增 runti
25. `agent-ui-contracts``index.ts` 只能做 barrel exportsevents / runtime / projection / messages / timeline / graph 必须按职责拆分。
26. 标准 UI 不得恢复旧私有 tree 命名或旧 Agent App `data-agent-run-projection-*` DOM;过程结构以 message parts、process timeline、execution graph 和 runtime facts 为标准 surface。
27. `agent-runtime-ui` 用户可见文案必须可由 labels / formatter props 注入;默认 fallback 不得替代宿主五语言 i18n。
28. Agent App 第二宿主必须支持注入标准 `AgentRuntimeClient`,并通过 `startTurn/readThread/cancelTurn/respondAction` 进入 App Server current 主链;没有标准 client 或显式 compat `api` 时必须 fail closed。
29. Agent App 标准 client adapter 不得依赖 `safeInvoke`、legacy task command string、renderer mock、desktop-host default mock 或独立 task read protocol。
30. `@limecloud/agent-runtime-client/sessionGateway` 必须提供 browser-safe 的 `createAgentRuntimeClientFromSessionGateway(...)`,把已有 App Server session gateway 适配成 `startTurn/readThread/cancelTurn/respondAction` lifecycle client;宿主 feature 不得复制这段通用映射,也不得从根入口导入该运行时值。
31. Agent App 默认运行页必须注入 App Server-backed 标准 runtime host options,通过 `agentSession/start` 创建 session,再进入标准 client adapter;页面回归必须证明默认 Host Bridge 调用 App Server current methods,且未调用 `agent_app_runtime_*` compat facade。
32. `agent_app_runtime_*` 不得停留在 DevBridge `bridgeTruthCommands`;只允许作为 no-mock compat residual 和显式兼容入口,且退出条件是所有既有桌面路径迁到标准 `AgentRuntimeClient` / App Server lifecycle 后删除 `src/lib/api/agentAppRuntime.ts`
最低验证入口:
```bash
npm --prefix packages/agent-runtime-projection run test
npx vitest run "src/components/agent/chat/projection/agentUiProjectionSummary.test.ts" "src/components/agent/chat/projection/conversationProjectionStore.test.ts" "src/components/agent/chat/projection/agentUiTeamWorkbenchViewModel.test.ts" --silent=passed-only --disableConsoleIntercept
npm --prefix packages/agent-runtime-ui run test
npm run test:contracts
```
`agentUiSubagentsViewModel.ts` 仍是主聊天 presentation residual 时,可额外跑对应产品侧定向测试;它只能证明 presentation residual 没退化,不能替代标准包验证:
```bash
npx vitest run "src/components/agent/chat/projection/agentUiProjectionSummary.test.ts" "src/components/agent/chat/projection/conversationProjectionStore.test.ts" --silent=passed-only --disableConsoleIntercept
npm run test:contracts
```
@@ -260,7 +285,7 @@ npm run test:contracts
## 下一步收缩顺序
1.`agent-ui-contracts` 增加更清晰的 event taxonomy / schema fixture,减少 string-only 事件漂移。
2. 第二宿主从 host run state adapter 继续推进到 `AgentRuntimeClient -> App Server current methods -> Agent App host run state` 的端到端接入
3. 保持并验证 `AgentRuntimeClient -> projectAgentUiState -> AgentUiProjectionView` 这条跨宿主标准链路,避免第二宿主回退成私有 UI projection
2. 第二宿主补 Agent App 专项 Electron fixture,直接证明 `AgentRuntimeClient -> Agent App host run state -> projectAgentUiState -> AgentUiProjectionView` 的完整链路;聚合 `verify:gui-smoke` 已作为桌面壳不回归证据,但不能替代第二宿主专项闭环
3. 继续收缩默认 `agent_app_runtime_*` compat facade;当前 DevBridge 已降为 no-mock compat residual,下一刀应删除或替换未注入标准 client 时的默认 fallback
4. 收缩主聊天旧 projection 的兼容导出名和旧生成物。
5.重复出现的 Team Workbench view model selector 分批下沉,保留宿主业务 action route
5. `agentUiSubagentsViewModel.ts` 的 host-neutral selector 分批下沉为标准 Subagents selector,保留宿主业务 action route,并删除 workspace / 产品测试里的旧命名 residual
+1 -9
View File
@@ -501,15 +501,7 @@ Skill 执行链路同样遵循单一命令边界。当前前端入口为 `src/li
- `preview_automation_schedule`
- `validate_automation_schedule`
Companion 桌宠链路同样遵循这条路径。当前主入口为 `src/lib/api/companion.ts`,统一承接:
- `companion_get_pet_status`
- `companion_launch_pet`
- `companion_send_pet_command`
Lime 主应用会在本地维护 `ws://127.0.0.1:45554/companion/pet` 的桌宠 companion 入口。前端如需感知桌宠连接状态,应继续通过 `companion-pet-status` 事件监听统一状态,不要在页面或 Hook 里自行直连本地 `WebSocket`
如果 companion 协议继续扩展,也应优先延续“Lime 做宿主、桌宠只收脱敏派生状态”的边界。例如 provider 概览相关能力,允许 Lime 通过 `companion_send_pet_command` 下发诸如 `pet.provider_overview` 这类脱敏摘要,并允许桌宠通过 `pet.open_provider_settings` 请求 Lime 聚焦主窗口并跳到 `设置 -> AI 服务商`,或通过 `pet.request_provider_overview_sync` 请求 Lime 立即重发最新的脱敏摘要;桌宠交互增强能力也应继续走这条主链,例如双击 / 三击桌宠后发出 `pet.request_pet_cheer``pet.request_pet_next_step`,或通过 `pet.request_chat_reply` 携带用户输入文本,请求 Lime 代为调用当前可聊天模型,再统一回写 `pet.show_bubble`;但不允许桌宠直接读取凭证文件、数据库或已下线的完整凭证 HTTP 接口。
Companion 桌宠链路已下线,分类为 `dead``src/lib/api/companion.ts`、全局 companion hook、设置页桌宠管理、桌宠能力偏好、provider overview 同步、默认 mock 与 `companion_get_pet_status` / `companion_launch_pet` / `companion_send_pet_command` 命令面均不得重新接回前端网关、Desktop Host、DevBridge 或 App Server current 主链。后续如果重新设计独立桌面伴随能力,必须重新定义 current 事实源和协议边界,而不是复活旧 `companion_*` 命令、`companion-pet-status` 事件或本地 `ws://127.0.0.1:45554/companion/pet` 入口。
## 命令契约的事实源
+8 -8
View File
@@ -29,8 +29,8 @@
- 如果 Playwright 工具当前还在 deferred surface,优先用 `ToolSearch` 的精确选择名,例如 `select:mcp__playwright__browser_click`;不要把 `playwright_browser_click``browser click` 之类同义词反复丢给 `ToolSearch`
- 能走真实后端就走真实后端;浏览器模式暂不支持或尚未桥接的能力,允许走 mock
- `verify:gui-smoke` 内部的 browser runtime 校验默认走无界面浏览器会话;它只证明主链可启动,不替代后续真实页面交互验证
- `lime-pet` 原生桌宠属于独立仓库与原生窗口壳,不纳入当前 WebView Playwright 的直接操控范围;在 Lime 主仓里只验证 `companion_*` API、状态事件与主窗口唤起链路,桌宠窗口移动、点击命中与原生层动画仍需额外手工 smoke
-果 companion 协议新增了 provider 摘要、桌宠回跳设置、桌宠主动请求同步,或双击 / 三击 / 文本对话触发的桌宠 LLM 交互事件,Playwright 续测只覆盖 Lime 主仓内的“状态事件是否触发”“是否跳到 `设置 -> AI 服务商`”“是否重发脱敏摘要”“是否调用宿主侧 LLM 代理逻辑”和“主窗口是否被唤起”,不在 WebView 层尝试直接操控原生桌宠 UI
- Companion 桌宠链路已在 Lime 主仓下线并归类为 `dead`Playwright 续测不再验证 `companion_*` API、`companion-pet-status` 状态事件、桌宠设置回跳或主窗口唤起链路
-未来重新设计独立桌面伴随能力,必须先定义新的 current 协议与 GUI 入口,再补对应 Playwright 覆盖;不得复用旧 `companion_*` 命令或本地 `companion/pet` 协议作为续测依据
- 共享网关控制页已下线,托盘也不再展示网关状态或地址;共享网关 `/v1/routes` 与 selector HTTP 路由也已下线,不再对“启动/停止网关、复制网关地址、路由/curl 示例、selector 路由、托盘运行态文案”做 GUI 续测;server 验证只关注标准 `/v1/messages``/v1/chat/completions` 主链,如需看运行时状态,走开发者页或实验页的诊断面板
- 旧设置页里的“安全与性能 / 容错配置”已经下线,不再对这些页签、表单或命令写入路径做 GUI 续测;如果还要验证提示路由,只围绕当前输入框 `get_hint_routes` 读取链与提示展示,不再寻找旧设置页入口
- 初装引导里的旧插件选择 / 插件安装 / 配置切换链路已经下线,不再对 `config-switch` 推荐安装、Provider Switch 页面或相关命令做 GUI 续测;当前 onboarding 只围绕现役语音体验流程验证
@@ -464,20 +464,20 @@ npm run verify:gui-smoke -- --include-knowledge-product-e2e --reuse-running
4. 验证工具开关恢复的是该话题最近一次 session runtime,而不是主题级 localStorage 默认值
5. 如首次切回旧话题时只能命中 fallback,再继续切换一次,确认第二次开始已优先走 runtime 恢复
### 话题 Team 恢复验证
### 话题 Subagents 恢复验证
1. 进入同一工作区中的两个话题
2. 在话题 A 里选择一个 builtin Team,在话题 B 里选择另一个 builtin 或 custom Team
2. 在话题 A 里选择一个 builtin Subagents profile,在话题 B 里选择另一个 builtin 或 custom profile
3. 在两个话题之间来回切换,必要时新建一个空白话题再切回
4. 验证 Team 选择器、摘要区和 Team Workbench 展示恢复的是该话题最近一次 `recent_team_selection`,而不是主题级 localStorage 的旧值
5. 对 custom Team 额外确认:切回后 label / description / roles 没丢;如果本轮是从 fallback 回填,继续切换一次确认第二次开始已优先走 runtime 恢复
6. 如果当前项目已有子代理或父会话上下文,再发送一条新消息,确认 Team Workbench 的 shadow 卡片与当前 Team 恢复一致,不会退回到全局 theme fallback;本轮如涉及 `harness.team_memory_shadow`,这里就是最小 GUI 续测锚点
4. 验证 Subagents profile 选择器、摘要区和子代理展示恢复的是该话题最近一次 `recent_team_selection`,而不是主题级 localStorage 的旧值
5. 对 custom profile 额外确认:切回后 label / description / roles 没丢;如果本轮是从 fallback 回填,继续切换一次确认第二次开始已优先走 runtime 恢复
6. 如果当前项目已有子代理或父会话上下文,再发送一条新消息,确认子代理影子卡片与当前 profile 恢复一致,不会退回到全局 theme fallback;本轮如涉及 `harness.team_memory_shadow`,这里就是最小 GUI 续测锚点
### 子代理 current 字段验证
1. 准备一个带 Team 或父子会话上下文的工作区,并触发一次子代理创建
2. 如果当前入口支持显式名称或工作目录,优先带上 `name` 与绝对 `cwd`;如果 UI 暂无显式入口,至少复用现有 flow 创建一个 child session,并在详情区观察其展示名与工作目录
3. 验证 child session / Team Workbench 优先显示显式 `name`,而不是退回 `agent_type`、profile label 或 task summary fallback
3. 验证 child session / Subagents 展示优先显示显式 `name`,而不是退回 `agent_type`、profile label 或 task summary fallback
4. 如果本轮涉及 `teamName`,确认 child 会回挂到当前 Team,上下文里能按该名字识别,不会出现重复成员或错挂到其它 Team
5. 验证 child 的 `working_dir` 与详情展示反映请求的绝对 `cwd`;如果请求非法相对路径,前端应看到明确失败,而不是静默回退父目录
6. 如果当前入口或调试面板暴露 `mode / isolation`,传入非空值时前端应看到明确 unsupported,而不是静默创建 child session
+2 -2
View File
@@ -186,7 +186,7 @@ npm run smoke:claw-chat-current-fixture
如果本轮是在清退插件中心模块,插件安装 / 管理 / UI / RPC 命令族也必须同步从 `src/lib/api` 网关、页面入口、设置入口、Rust 注册、DevBridge 与默认 mock 中撤掉;当前 GUI 不再保留插件中心或动态插件侧栏入口。最低校验至少包含 `npm run test:contracts``npm run governance:legacy-report` 与受影响前端回归。
如果本轮涉及 `companion_*` 桌宠命令族,还要同步检查本地 companion `WebSocket` 入口、前端 `src/lib/api/companion.ts` 网关、Rust 注册、治理目录册以及浏览器模式 mock 返回形态;浏览器模式下这组命令默认也要保持可 mock,不要让桌宠接入把默认页面渲染链路卡死
`companion_*` 桌宠命令族已下线并归类为 `dead`。如果本轮发现 `companion_get_pet_status``companion_launch_pet``companion_send_pet_command``src/lib/api/companion.ts`、桌宠设置页、桌宠偏好或 `companion-pet-status` 事件重新出现在生产入口、Desktop Host、DevBridge、默认 mock 或 App Server 主链,应先按旧路回流处理并删除;不得把它们恢复成浏览器模式 mock fallback
如果本轮涉及 team runtime 工具面或主线程用户消息工具,还要同步检查 Rust catalog / inventory、runtime 注册、浏览器 fallback mock 与前端 tool display`Agent / TeamCreate / TeamDelete / SendMessage / ListPeers` 必须保持同一组 current surface`SendUserMessage` 也必须继续停留在 current 主线程工具面,不要把已删除的 `SubAgentTask` compat 工具重新接回 Rust catalog、runtime 注册、mock 或前端 tool display。
@@ -489,7 +489,7 @@ CI 里的 `.github/workflows/quality.yml` 结果摘要现在也会透出 `bridge
- 修改 `execution_runtime.recent_access_mode / recent_theme / recent_session_mode / recent_gate_key / recent_run_title / recent_content_id` 恢复语义,或前端 `harness.access_mode / harness.theme / harness.session_mode / harness.gate_key / harness.run_title / harness.content_id` steady-state 去重逻辑
- 修改首页 / 工作区进入 `Claw` 时的首条自动发送上下文,例如 `initialUserPrompt``initialAutoSendRequestMetadata``harness.service_skill_launch`
- 修改 `site_*` 站点适配器命令族,例如 `site_recommend_adapters``site_get_adapter_launch_readiness``site_import_adapter_yaml_bundle``site_run_adapter`
- 修改 `companion_get_pet_status``companion_launch_pet``companion_send_pet_command`,或调整 Lime 与独立桌宠之间的本地 companion 协议(例如 `pet.provider_overview``pet.open_provider_settings``pet.request_provider_overview_sync``pet.request_pet_cheer``pet.request_pet_next_step``pet.request_chat_reply`
- 发现已下线的 `companion_get_pet_status``companion_launch_pet``companion_send_pet_command``companion-pet-status` 或旧桌宠本地 companion 协议回流
- 修改自动化设置 App Server method 族,例如 `automationJob/list``automationJob/create``automationJob/update``automationJob/health``automationJob/runHistory`;旧 `get_automation_*` / `*_automation_job` 命令已退役,只能作为负向回归扫描对象
- 修改浏览器资料 / 环境预设命令族,或调整它们在 `mockPriorityCommands` 里的优先级
- 修改浏览器连接器命令族,例如安装目录、启用状态、系统连接器、浏览器动作配置、扩展安装状态、打开 Chrome 扩展 / 远程调试页,或主动断开扩展连接
+4
View File
@@ -33,3 +33,7 @@
| CCD-011 | 2026-05-09 | Task Center / Root Patch Scripts | 根目录遗留 `patch_dark.js``patch_flare.js``patch_navbar.js` 只是一次性字符串替换脚本;当前 Task Center Chrome 样式已收敛到 `ChatNavbar.tsx` 的 token 化主链,脚本目标 `taskCenterChromeShellClassName` 已不再存在 | 继续保留会让后续开发误判为可运行维护脚本,并可能把旧的字符串替换方式重新带回 UI 主链 | 已删除 3 个脚本,并在 `legacySurfaceCatalog` 中补 `root-task-center-patch-scripts` 守卫;后续不得恢复根目录补丁脚本式 UI 修改,Task Center 样式只沿现有组件和 token 主链演进 | `src/components/agent/chat/components/ChatNavbar.tsx` | 已完成 |
| CCD-012 | 2026-06-09 | Frontend DevBridge / Command Governance | `src/lib/dev-bridge/**` 容易被误判为旧 Rust / Tauri DevBridge 的同类退场对象;当前实际应拆成 `current` renderer bridge`safeInvoke`、HTTP client、`app_server_handle_json_lines`、事件监听、可用性探测)与 `compat / deprecated` 命令 policy / mock fallback 两部分治理。后续治理事实源已固定到本条执行规则与 production command current 迁移计划,不以聊天、handoff 或临时计划备注为准 | 若后续清命令时整目录删除,会破坏 Electron Desktop Host / App Server JSON-RPC 的 renderer 传输链;若继续把已迁旧命令留在 `commandPolicy.ts``mockPriorityCommands.ts`、desktop-host mock 或旧 smoke 正向证据中,又会让 retired 命令通过 mock / policy 回流成假 current | 后续每个命令组迁到 App Server / Electron Desktop Host current 后,同轮检查并收缩 `commandPolicy.ts``mockPriorityCommands.ts`、desktop-host mock、旧 smoke、catalog 和 retired guard;删不动且跨命令组长期存在的 legacy command policy / mock residual 必须同时登记到对应执行计划和本条 `CCD-012`,写清命令名、阻塞文件、分类、退出条件和下一次验证入口。下一小步按 `internal/exec-plans/production-command-current-migration-plan.md` 当前主线优先处理 P6 session files 工作台写读链或 P14 recording / transcription / polish / output side-effect,并把残留旧命令字符串分类为 `compat / deprecated``dead / retired guard-only``test-only`,不得只写“后续清理” | `internal/aiprompts/governance.md` / `internal/aiprompts/commands.md` / `internal/aiprompts/parallel-agent-collaboration.md` / `internal/aiprompts/quality-workflow.md` / `internal/roadmap/appserver/README.md` / `internal/exec-plans/production-command-current-migration-plan.md` | 进行中 |
| CCD-013 | 2026-06-10 | Rust 主 crate / Tauri Wrapper | `lime-rs/src/**` 是脱离 cargo 构建图的孤儿目录(workspace 只含 `crates/*``src/` 没有 `lib.rs` / `main.rs` 也没有 `[package]` 段),曾沉积约 18.7 万行 / 113 个 `#[tauri::command]` / 19 个 `*_cmd.rs`,过去几天通过 50+ 个 TW-Q* 子任务串行抠删 | 持续制造治理噪音、误导接手 Agent、长期占据并行进程精力;删除是纯文本操作 | 已物理删除整个目录(含 `commands/services/dev_bridge/app/agent/skills/voice/tray/config/theme/data/tests/agent_tools` 与顶层 6 个 `.rs`),唯一活引用 `agent_tools/{catalog,execution,inventory}.rs` 已迁到 `lime-rs/crates/agent/src/agent_tools/`;3 份相关执行计划标 superseded;治理脚本 `check-app-server-client-contract.mjs` 删 16 个失效 invariant、`check-harness-contracts.mjs` 删 backend mapping 部分、`legacyToolPermissionGuard.test.ts` 移除 `lime-rs/src` 扫描根、`rustCommandsCurrentBoundary.test.ts` 重写为目录不可恢复守卫;AGENTS.md 12/13 条收紧;验证:`cargo check --workspace` 通过、`npm run test:contracts` 全过、`npx vitest run src/lib/governance` 187/187 通过 | `internal/exec-plans/tauri-wrapper-quick-cleanup-queue.md`superseded/ `internal/exec-plans/tauri-wrapper-command-inventory.md`superseded/ `internal/exec-plans/rust-commands-current-migration-cleanup-plan.md`superseded/ `src/lib/governance/rustCommandsCurrentBoundary.test.ts` | 已完成 |
## CCD-012 residual log
- `2026-06-10``agent_app_runtime_start_task``agent_app_runtime_cancel_task``agent_app_runtime_get_task``agent_app_runtime_submit_host_response` 已从 `src/lib/dev-bridge/commandPolicy.ts#bridgeTruthCommands` 降为 `noMockFallbackCompatCommands`。当前分类:`compat / fail-closed residual`。阻塞文件:`src/lib/api/agentAppRuntime.ts` 显式兼容网关、尚未迁完的既有桌面路径、显式兼容测试;`AgentRuntimeCapabilityHost` 默认已改为无标准 `AgentRuntimeClient` / 无显式 compat `api` 时 fail closed,不再隐式回退旧 facade。退出条件:所有 Agent App 宿主入口迁到标准 `AgentRuntimeClient` / App Server lifecycle 后,删除 `src/lib/api/agentAppRuntime.ts`,并把四个命令移出 no-mock compat 集合。验证入口:`src/features/agent-app/runtime/agentRuntimeCapabilityHost.test.ts``src/lib/dev-bridge/commandPolicy.test.ts``scripts/check-command-contracts.mjs``npm run test:contracts`
+2 -2
View File
@@ -211,14 +211,14 @@ Agent App 不只复用 UI runtime start/status/stop,而是复用 Agent turn ru
8. 前端 `src/lib/api/agentRuntime/threadClient.ts` 的 turn lifecycle 已可消费标准 `AgentRuntimeClient` facade`submit / cancel / respond / read` 先从旧 UI 请求 shape 投影为 App Server current 参数,再委托 `startTurn / cancelTurn / respondAction / readThread`。该文件仍是迁移期 compat gateway,负责旧 DTO、事件路由、read model 投影、file checkpoint / queue / compact / replay 等尚未进入标准 client 的业务面,不允许继续扩展成第二套 runtime SDK。
9. 根依赖、TypeScript paths 与 Vite alias 已接入 `@limecloud/agent-runtime-client`,避免标准包只停留在 package 目录而无法被前端 current 网关消费。
10. `@limecloud/agent-runtime-projection` 已承接 host-neutral 的 Agent UI projection event index / scope selector / latest selector。主聊天 `conversationProjectionStore` 只保留本地 external store、stream diagnostics 和兼容导出名,事件筛选与索引不再在主聊天目录内作为公共事实解释继续扩展。
11. `@limecloud/agent-runtime-projection` 已承接 host-neutral 的 Agent UI summary selectors:事件类型分组、action / task / artifact / evidence / diagnostics 计数、notable latest events、Team Workbench surface / lane 聚合和 artifact latest lookup。主聊天 `agentUiProjectionSummary.ts` 只保留本地化 label、展示文案 formatter 和兼容导出名,不再作为这些 selector 的事实源。
11. `@limecloud/agent-runtime-projection` 已承接 host-neutral 的 Agent UI summary selectors:事件类型分组、action / task / artifact / evidence / diagnostics 计数、notable latest events、Subagents surface / lane 聚合和 artifact latest lookup。主聊天 `agentUiProjectionSummary.ts` 只保留本地化 label、展示文案 formatter 和兼容导出名,不再作为这些 selector 的事实源。
12. `internal/aiprompts/agent-ui-runtime-standard.md` 已成为四包职责、宿主接入、App Server runtime 配合、主聊天 projection 迁移分类和守卫要求的集中事实源;后续改标准包或主聊天 projection 前应先读该文档。
13. `scripts/check-app-server-client-contract.mjs` 已守住标准包名入口,扫描根依赖、lockfile、TypeScript paths、Vite alias 和 `packages/*/package.json`,防止早期讨论包名回流成新的物理包或 alias。
14. Agent App Runtime 已作为第二个真实宿主基线消费标准 projection:`agentRunProjectionState.ts` 从 host run state 生成标准 `AgentUiProjectionState``AgentRunProjectionPanel.tsx` 直接渲染 `AgentUiProjectionView`,宿主 summary / action callback / artifact presentation 保留在 Agent App adapter 层。
退出条件补充:
1. 新 Agent UI 页面默认走 `AgentRuntimeClient -> execution events -> projectAgentUiState -> AgentUiProjectionView`
1. 新 Agent UI 页面默认走 `AgentRuntimeClient -> projectAgentUiState -> AgentUiProjectionView`,中间的 execution events 适配由宿主边界负责
2. 不允许把组件本地树或 UI 本地状态定义成 runtime fact source。
3. `readThread` 当前映射 `agentSession/read`;独立 `readTask` 只能等 App Server current method 出现后再接入。
4. 至少两个真实宿主复用 `@limecloud/agent-ui-contracts``@limecloud/agent-runtime-projection``@limecloud/agent-runtime-ui` 后,才能继续收缩旧主聊天专用 projection / UI 壳。
@@ -118,7 +118,7 @@ AgentUI 可以消费 raw stream event、profile event、snapshot 和 read model
RuntimeEvent / AgentRuntimeProfileEvent
-> ThreadReadModel / TaskSnapshot / EvidenceSummary
-> Agent UI envelope projection
-> Workspace / Harness / Timeline / Task Capsule / Team Workbench
-> Workspace / Harness / Timeline / Task Capsule / Subagents
```
P1 允许 AgentUI 对 `schemaVersion = lime-profile-0.4.0` 的 dotted profile event 静默保持 stream 活跃;P2 开始必须把这些 facts 归入 `ThreadReadModel`,再由 AgentUI 只读展示。
@@ -77,7 +77,7 @@ AgentRuntime 标准正好把这些能力定义成 execution facts
| Permission / sandbox / hook / process | Claw/ToolRuntime 有相关事实,但 Agent App Host 可见度不足 | 将 permission、sandbox、hook、process 事件进入 projection diagnostics / evidence,不让 App 自建权限判断 | P1 |
| Tool inventory snapshot | 标准要求当前 scope 下可用 tools / policy / capabilities 可解释 | `lime.skills / lime.tools / lime.models` 能力目录需要从 Host 投影给 App,App 只显示可用/不可用 | P1 |
| History / recovery / snapshot repair | 单会话全流程仍红,说明状态递进和 task snapshot 稳定性还不够 | 打开历史 App task 时先用 snapshot 恢复,再按 sequence 补事件 | P2 |
| Subagents / jobs / remote channels | 标准覆盖完整委派和远程工作,但内容工厂当前 P0 不是多 agent workbench | 先保持 solo task + required Skills;等 runtime 暴露 parent-child graph 后再进入 Team Workbench | P2 |
| Subagents / jobs / remote channels | 标准覆盖完整委派和远程工作,但内容工厂当前 P0 不是多 agent workbench | 先保持 solo task + required Skills;等 runtime 暴露 parent-child graph 后再进入 Subagents | P2 |
## 6. 暂缓或禁止
@@ -88,7 +88,7 @@ AgentRuntime 标准正好把这些能力定义成 execution facts
| 内容工厂直连 OpenAI-compatible API 或 `LIME_GATEWAY_*` 完成主流程 | dead | 只能得到模型补全,拿不到 Skills、Tools、routing、cost、evidence、HITL。 |
| 每个 App 复制 Claw `*_skill_launch.rs` | dead | 破坏 Claw capability catalog 复用,制造权限和证据旁路。 |
| App 内完整实现 AgentUI runtime store | dead | UI projection 只能在 Host 层统一,App 只能消费标准视图。 |
| Team Workbench / remote teammate 全量默认进入每个 App | deferred | 需要 subagent/job/channel runtime facts 先稳定。 |
| Subagents / remote teammate 全量默认进入每个 App | deferred | 需要 subagent/job/channel runtime facts 先稳定。 |
| 用最终正文反推 artifact、Skill 成功或 evidence pass | deprecated | 标准明确要求 missing facts 标为 `unknown/unavailable/stale/blocked`,不能猜。 |
## 7. current / compat / deprecated / dead 分类
@@ -33,7 +33,7 @@ AgentUI v0.6.1 定义的是 runtime-first projection 标准,核心能力包括
- Runtime event projection`run.* / text.* / reasoning.* / tool.* / action.* / queue.* / artifact.* / evidence.* / session.* / team.*` 等 event class。
- 标准表面:Composer、Message Parts、Runtime Status、Tool UI、Human-in-the-loop、Task Capsule、Artifact Workspace、Timeline / Evidence、Session / Tabs。
- Team WorkbenchTeam Roster、Work Board、Delegation Graph、Handoff Lane、Worker Notifications、Review Lane、Teammate Transcript、Background / Remote Teammate、Team Policy。
- SubagentsSubagent Threads、Delegation Calls、Handoff / Review Activity、Worker Notifications、Teammate Transcript、Background / Remote Teammate、Team Policy。
- Client implementationprojection store、progressive rendering、session hydration、queue vs steer、controlled writes、performance metrics。
- Agent Runtime profile tests:验证 `RuntimeEvent / ThreadReadModel / TaskSnapshot / EvidencePack` 能投影到 UI,同时 UI 不变成 runtime truth。
@@ -79,7 +79,7 @@ Agent App MVP 必须先满足:真实 runtime facts -> Host Run UI -> 业务 ar
| Evidence / Review / Replay lane | Evidence refs 已可投影,完整 Evidence panel、review verdict、replay lane 未统一 | 使用 `agent_runtime_export_evidence_pack` 作为单一事实源,Review / Replay 不再各自重算状态 | P1 |
| Session hydration | AgentRuntime 有 ThreadReadModelAgent App Host Run UI 还没有完整旧 session 渐进恢复体验 | 打开历史任务时先 shell / snapshot / recent process,再懒加载 timeline、tool output、artifact、evidence | P2 |
| Performance metrics | completion gates 记录结果,但 Host UI 尚未系统展示 submit-to-status / first-text / paint latency | 在 projection 或 evidence 中保留 `metric.changed`,普通 UI 低噪声,诊断视图可检查 | P2 |
| Team Workbench | Lime AgentRuntime / Claw 有 subagent、team prompt、spawn/wait/close 能力,但 Agent App Host UI 未投影 team surfaces | 只有当 runtime 暴露 parent/child lineage、worker notification、team phase 后,才进入 roster / delegation / review lane | P2 |
| Subagents | Lime AgentRuntime / Claw 有 subagent、team prompt、spawn/wait/close 能力,但 Agent App Host UI 未投影 subagent surfaces | 只有当 runtime 暴露 parent/child lineage、worker notification、team phase 后,才进入 threads / delegation / review activity | P2 |
## 5. 暂缓或不应直接采用的部分
@@ -91,7 +91,7 @@ Agent App MVP 必须先满足:真实 runtime facts -> Host Run UI -> 业务 ar
| AgentUI event schema 直接替代 AgentRuntime | 禁止 | AgentUI 是 projection schema,不拥有 execution、policy、artifact、evidence truth。 |
| App 内实现完整 AgentUI runtime | 禁止 | 会让业务 App 变成第二套 Agent Runtime,回到“每个 App 自建 AI 同事”的问题。 |
| 业务 App 直连模型 / provider API | 禁止 | 绕过 Lime model routing、Skills、ToolRuntime、usage、cost、policy、Evidence。 |
| Team Workbench 全量表面一次性落地 | 暂缓 | 需要 runtime 先稳定暴露 `agent.spawned / worker.notification / team.changed / handoff / review` facts。 |
| Subagents 全量表面一次性落地 | 暂缓 | 需要 runtime 先稳定暴露 `agent.spawned / worker.notification / team.changed / handoff / review` facts。 |
| Remote teammate / background teammate UI | 暂缓 | 需要 remote task / background scheduler 的 durable facts;不能用普通 running 文案伪造。 |
| Work Board 作为所有 Agent App 默认壳 | 暂缓 | 不是所有业务 App 都是看板心智;只能在 runtime 有 work item truth 时投影。 |
@@ -108,7 +108,7 @@ Agent App MVP 必须先满足:真实 runtime facts -> Host Run UI -> 业务 ar
| Artifact Workspace | 内容工厂 workspace patch、artifact refs 已接入 | 通用 preview / edit / version / diff / export 还未抽象 | 内容工厂可先写回业务对象,通用 artifact workspace 需要补。 |
| Timeline / Evidence | facts rail、timeline、Evidence refs 已可见 | Evidence detail / replay / review lane 仍分散 | 可作为 MVP,但完整 evidence 审计仍要回到 EvidencePack。 |
| Session / Tabs | AgentRuntime 有 thread read snapshot | Agent App Host UI 未实现旧任务渐进恢复和 tab resource policy | 暂不作为内容工厂阻塞项,列入 Host 层后续。 |
| Team Workbench | Claw / AgentRuntime 有 subagent 和 team runtime 基础 | Agent App 无 roster / delegation / worker notification / handoff lane | 暂缓全量;先保证 solo_run 与 required Skills。 |
| Subagents | Claw / AgentRuntime 有 subagent 和 team runtime 基础 | Agent App 无 threads / delegation / worker notification / handoff activity | 暂缓全量;先保证 solo_run 与 required Skills。 |
| Diagnostics / Metrics | Smoke evidence、completion audit、cost/usage facts 已有 | 用户可见 latency metrics、safe diagnostics surface 不完整 | 先用于测试和 evidence,后续进入 Host 诊断视图。 |
## 6.1 继续审计:Lime 已有 AgentUI 投影资产
@@ -120,7 +120,7 @@ Agent App MVP 必须先满足:真实 runtime facts -> Host Run UI -> 业务 ar
| 标准 event / owner / scope / surface 类型 | `src/components/agent/chat/projection/agentUiEventProjection.ts` 定义 `AgentUiProjectionEvent``AgentUiEventClass``AgentUiSurface``AgentUiRuntimeEntity``AgentUiTopology` | 不应在 Agent App 里再定义一套 AgentUI 类型。 |
| Claw runtime event -> AgentUI projection adapter | `buildAgentUiProjectionEvents(event, context)` 已覆盖 `run.* / text.delta / reasoning.delta / tool.* / action.* / queue.changed / artifact.* / evidence.changed / team.*` | Agent App 下一刀应接入或复用该 adapter,而不是从 `task:*` 直接渲染到底。 |
| Projection store | `conversationProjectionStore.ts` 提供 record / select by type / surface / scope / tool / action / artifact / evidence | Host Run UI 可使用同一个 projection store 或同构 reducer,避免第二套 UI state owner。 |
| Team Workbench 投影与视图模型 | `agentUiTeamWorkbenchViewModel.ts``AgentUiTeamWorkbenchSurfaceView.tsx` 已存在 | Team Workbench 不是零实现;缺口是 Agent App runtime facts 尚未投影和挂到 Host Run UI。 |
| Subagents 投影与视图模型 | `agentUiSubagentsViewModel.ts``AgentUiProjectionState.subagents``SubagentsView` 已存在 | Subagents 不是零实现;缺口是 Agent App runtime facts 尚未投影和挂到 Host Run UI。 |
| Claw stream 运行时接入 | `agentStreamRuntimeHandler.ts` 调用 `buildAgentUiProjectionEvents` 并记录 projection events | Claw 已经证明 stream -> AgentUI projection 的路径可行;Agent App 应复用这条路径。 |
| Team runtime 接入 | `team-workspace-runtime/runtimeEventSubscriptions.ts` 已把 subagent/team stream 记录为 AgentUI projection events | Agent App 的 team / subagent 后续应收敛到同一 projection,而不是另写 roster / worker notification。 |
| 后端 Agent App task projection first-cut | 当前脏工作树中 `runtime_turn.rs` 已把 profile/runtime events 投影为 `task:runtimeEvent` payload,含 `taskEvents``runtimeEvent``streamKind`、artifact/evidence/action/tool 映射 | 后端已有 first-cut,但仍是 Agent App task event 形态;前端还需要桥接到 AgentUI event class。 |
@@ -141,7 +141,7 @@ Agent App MVP 必须先满足:真实 runtime facts -> Host Run UI -> 业务 ar
2. 后端 task projection first-cut 仍输出 `task:*` 事件;前端 bridge 已可标准化为 `run.* / tool.* / action.* / artifact.* / evidence.*`,但尚未挂入 Host UI。
3. Ordered parts 的 state adapter、view model 和独立 panel 已可按 projection `sequence / partId / toolCallId / actionId` 排序,但现有 Host Run 抽屉尚未用它替换 `timeline + thinking + execution + output` 分区。
4. HITL 只有 request facts 和 submit host response 命令,Host Run UI 还缺 `action.required -> action.resolved` 的受控卡片闭环。
5. 现有 Claw AgentUI projection 的 Team / Remote / Background 能力不能直接宣称 Agent App 已支持;需要 Agent App runtime facts 接入后才算。
5. 现有 Claw AgentUI projection 的 Subagents / Remote / Background 能力不能直接宣称 Agent App 已支持;需要 Agent App runtime facts 接入后才算。
## 6.2 Schema parity 与 Agent App 可用子集
@@ -181,7 +181,7 @@ P1/P2 才进入的子集:
2. **Artifact Workspace**preview、edit、version、diff、export 必须先由 Host artifact service 持有。
3. **Evidence Review / Replay Lane**:必须消费 EvidencePack / review / replay 事实,不能 UI 推断。
4. **Session Hydration**:历史任务先恢复 snapshot,再懒加载 full timeline / tool output / evidence。
5. **Team Workbench**:等 Agent App runtime 暴露 parent/child lineage、worker notification、handoff、review facts 后再启用。
5. **Subagents**:等 Agent App runtime 暴露 parent/child lineage、worker notification、handoff、review facts 后再启用。
## 7. 对内容工厂和后续 Agent App 的落地规则
@@ -219,7 +219,7 @@ P0: Agent App Host -> existing AgentUI projection bridge
| AgentUI 标准要和 Lime 对比,找出哪些可用于 Agent App | 本文档第 3-6.2 节已经按能力、surface 与 schema parity 分类 | done | 保持文档随实现更新。 |
| Lime 不是所有 AgentUI 都支持 | 第 4、5、6、6.1、6.2 节列出缺口和暂缓项 | done | 缺口进入 P0/P1/P2。 |
| Agent App 不应每个 App 自己实现 AI 同事 | 第 1、7 节明确 Host Run UI 是 currentApp 内 fallback 是 compat | done | 后续用守卫/测试封住 App 私有运行 UI 回流。 |
| 需要复用 Lime / Claw 已有 UI 封装 | 第 6.1 节确认 Claw 已有 AgentUI projection adapter/store/Team view model;新增 `agentUiProjectionBridge.ts` 复用同一 `AgentUiProjectionEvent` 类型;新增 `agentUiProjectionViewModel.ts` 生成 Host Run ordered parts;新增 `AgentRunProjectionPanel.tsx` 独立渲染 projection view;新增 `agentRunProjectionState.ts` 适配现有 Host Run state | partial | 现有 `AgentRunHostDrawer.tsx` 还没接入 projection panel。 |
| 需要复用 Lime / Claw 已有 UI 封装 | 第 6.1 节确认 Claw 已有 AgentUI projection adapter/store/Subagents view model;新增 `agentUiProjectionBridge.ts` 复用同一 `AgentUiProjectionEvent` 类型;新增 `agentUiProjectionViewModel.ts` 生成 Host Run ordered parts;新增 `AgentRunProjectionPanel.tsx` 独立渲染 projection view;新增 `agentRunProjectionState.ts` 适配现有 Host Run state | partial | 现有 `AgentRunHostDrawer.tsx` 还没接入 projection panel。 |
| 内容工厂需要真实 AI Agent 能力,不是直调 API | 现有 roadmap / completion audit 记录 required Skills、model、usage、cost、artifact、evidence gates;本轮未重跑全流程 | partial | 继续由 runtime/GUI 持有写集进程跑 completion E2E。 |
| 运行过程要有思考、执行、工具、Skill、流式输出且不消失 | Host first-cut 已有 `ThinkingBlock``InlineToolProcessStep`、Markdown renderer`agentRunProjectionState.ts -> agentUiProjectionBridge.ts -> agentUiProjectionViewModel.ts -> AgentRunProjectionPanel.tsx` 已形成独立 projection renderer 链路;projection view model 已保证 running tool/process 默认展开、终态工具/status 折叠、最终成稿保持展开;projection panel 已把 Artifact / Evidence refs 渲染为独立卡片 | partial | 让现有 Host drawer 使用 projection panel,并补 Claw renderer 深复用。 |
| 模型、Token、费用等要支持 | `runtimeProcess.model / usage / cost` 和 Host metric cards 已有;Agent App projection bridge 已支持 `metric.changed` diagnosticsview model / panel 可把模型、Token、费用指标渲染为 diagnostics 卡片 | done/first-cut | Host Drawer 接入后继续补 cost/limit diagnostics 视觉验收。 |
@@ -277,7 +277,7 @@ P0: Agent App Host -> existing AgentUI projection bridge
AgentUI 中 **核心 runtime projection、message parts、Tool UI、HITL、Artifact、Evidence、Task Capsule、Runtime Profile tests** 都适合进入 Agent App;但进入方式必须是 Lime Host 统一封装,而不是每个 App 直接引入一套 AgentUI 实现。
Lime 现在已经具备 `solo_run + required Skills + Host Run UI first-cut` 的基础,足够支撑内容工厂继续验证真实业务流程;但还不能宣称完整支持 AgentUI v0.6.1,因为 ordered parts、HITL 控件、Artifact Workspace、Evidence Review/Replay、Session Hydration、Team Workbench 仍未完整落地。
Lime 现在已经具备 `solo_run + required Skills + Host Run UI first-cut` 的基础,足够支撑内容工厂继续验证真实业务流程;但还不能宣称完整支持 AgentUI v0.6.1,因为 ordered parts、HITL 控件、Artifact Workspace、Evidence Review/Replay、Session Hydration、Subagents 仍未完整落地。
最终产品边界保持不变:
@@ -34,11 +34,11 @@ agent_runtime_submit_turn
| 要求 | 状态 | 证据 | 说明 |
| --- | --- | --- | --- |
| 分析 Codex / Claude Code / 公开方案并形成 Lime 标准化方向 | done | `internal/roadmap/agentruntime/prd.md:21` | PRD 固定“公开标准 + Lime 严格 profile + current runtime 实现映射”。 |
| 分析公开方案 / Claude Code / 运行时实现参考并形成 Lime 标准化方向 | done | `internal/roadmap/agentruntime/prd.md:21` | PRD 固定“公开标准 + Lime 严格 profile + current runtime 实现映射”。 |
| 在 Lime `internal/roadmap/agentruntime` 写 PRD、背景、目的、收益、用户故事 | done | `internal/roadmap/agentruntime/prd.md:8``internal/roadmap/agentruntime/prd.md:43``internal/roadmap/agentruntime/prd.md:96``internal/roadmap/agentruntime/prd.md:221` | PRD 覆盖背景、目标、收益、用户故事、范围和验收。 |
| 增加架构图、流程图、时序图 | done | `internal/roadmap/agentruntime/diagrams.md:7``internal/roadmap/agentruntime/diagrams.md:56``internal/roadmap/agentruntime/diagrams.md:84` | 图纸集覆盖总体架构、相邻协议协同、主流程、submit turn、tool approval、evidence export、task retry、remote/subagent、UI 只读投影。 |
| 增加测试用例文档 | done | `internal/roadmap/agentruntime/test-cases.md:1` | 测试矩阵覆盖 schema、identity、read model、routing、tool/action、evidence/replay/review、相邻协议、GUI smoke 和治理守卫。 |
| 更新 AgentUI 匹配方案 | done | `src/components/agent/chat/projection/agentUiProjectionSummary.ts``src/components/agent/chat/projection/agentUiTeamWorkbenchViewModel.ts``src/components/agent/chat/components/AgentUiTeamWorkbenchSurfaceView.tsx` | AgentUI current projection 已接 key-based presentation mapperTeam Workbench / Harness / Reliability / Artifact timeline 已消费 mapper。 |
| 更新 AgentUI 匹配方案 | done | `src/components/agent/chat/projection/agentUiProjectionSummary.ts``src/components/agent/chat/projection/agentUiSubagentsViewModel.ts``packages/agent-runtime-ui/src/subagents.tsx` | AgentUI current projection 已接 key-based presentation mapperSubagents / Harness / Reliability / Artifact timeline 已消费 mapper。 |
| 对齐 AgentContext / AgentEvidence / AgentPolicy | done | `internal/roadmap/agentruntime/adjacent-protocols.md:1``lime-rs/src/services/runtime_agent_profile_projection_service.rs:16``lime-rs/src/services/runtime_evidence_pack_service.rs` | 相邻协议被固定为 owner refs;Runtime 只串联执行事实,不吞并上下文、策略和证据 schema。 |
| 修复 `agentruntime_profile` private module 编译错误 | done | `lime-rs/src/commands/aster_agent_cmd/mod.rs:340``lime-rs/src/commands/aster_agent_cmd/mod.rs:400` | 模块保持私有实现,向 crate 内 re-export `AgentRuntimeProfileEvent/Stream`,避免 service 直接依赖私有 module path。 |
| 拆分 `runtime_evidence_pack_service.rs` | done | `lime-rs/src/services/runtime_evidence_request_telemetry_service.rs:1``lime-rs/src/services/runtime_agent_profile_projection_service.rs:1``lime-rs/src/services/runtime_evidence_completion_audit_service.rs:1``lime-rs/src/services/runtime_evidence_modality_contract_service.rs:1``lime-rs/src/services/runtime_evidence_auxiliary_runtime_service.rs:1``lime-rs/src/services/runtime_evidence_verification_service.rs:1``lime-rs/src/services/runtime_evidence_observability_service.rs:1``lime-rs/src/services/runtime_evidence_gap_service.rs:1``lime-rs/src/services/runtime_evidence_pack_output_service.rs:1``lime-rs/src/services/runtime_evidence_markdown_locale_service.rs:1``lime-rs/src/services/runtime_evidence_pack_service_tests.rs:1` | 已拆出 request telemetry、profile projection、completion audit / controlled GET evidence、modality contract、auxiliary runtime、verification / artifact validator、observability / signal coverage、known gaps、pack output renderer、Markdown locale copy、artifact index、JSON/path/tool helper 与大体量单测 fixture;主编排已降到约 377 行。 |
@@ -55,7 +55,7 @@ agent_runtime_submit_turn
| P3 Evidence 同源导出 | done | `lime-rs/src/services/runtime_evidence_pack_service.rs``lime-rs/src/services/runtime_evidence_request_telemetry_service.rs``lime-rs/src/services/runtime_replay_case_service.rs:128``lime-rs/src/services/runtime_analysis_handoff_service.rs:209` | `evidence_runtime_should_export_agent_runtime_profile_spine`、request telemetry 空摘要测试、replay/analysis/review 定向测试 | Evidence facts JSON 同源完成;Markdown presentation locale 属于 PX,已完成 current 结构标题与核心标签。 |
| P4 Task / routing / permission / tool | done | `lime-rs/src/commands/aster_agent_cmd/agentruntime_profile.rs:294``lime-rs/src/commands/aster_agent_cmd/runtime_task_profile.rs:52``lime-rs/src/commands/aster_agent_cmd/runtime_turn.rs:6001` | `routing_events_match_single_candidate_fact_shape``runtime_tool_profile_should_follow_real_tool_start_and_end_once``runtime_tool_profile_should_fallback_to_item_tool_call_failure``collect_runtime_request_resolution_side_events_should_emit_routing_not_possible` | Policy allow/deny/ask facts 已进入 profile;更细的 obligation UI 展示可后续增强。 |
| P5 Subagent / Job / Remote Channel | done | `lime-rs/src/services/runtime_agent_profile_projection_service.rs:385``lime-rs/src/services/runtime_agent_profile_projection_service.rs:467``lime-rs/src/services/runtime_agent_profile_projection_service.rs:648` | `evidence_runtime_should_export_subagent_parent_child_profile_events``evidence_runtime_should_export_job_profile_events_from_owner_runs``evidence_runtime_should_export_job_item_profile_events_from_owner_run_metadata``evidence_runtime_should_export_remote_channel_resume_repair_profile_events` | 已覆盖 owner run / parent-child / resume-repair facts;真实远端端到端恢复可作为产品级 E2E 后续。 |
| PX 全球本地化守卫 | done/current, weak/legacy | `src/components/agent/chat/projection/agentUiProjectionSummary.ts``src/i18n/resources/*/agent.json``lime-rs/src/services/runtime_evidence_markdown_locale_service.rs` | `agentUiTeamWorkbenchViewModel` locale 单测、5 locale key count 一致、i18n namespace tests、Evidence/Replay/Analysis/Review Markdown locale Rust 定向测试 | AgentRuntime projection 与 current Markdown 导出结构标题已本地化;legacy diagnostics 和正文级 copy 完整化不是本轮完成口径。 |
| PX 全球本地化守卫 | done/current, weak/legacy | `src/components/agent/chat/projection/agentUiProjectionSummary.ts``src/i18n/resources/*/agent.json``lime-rs/src/services/runtime_evidence_markdown_locale_service.rs` | `agentUiSubagentsViewModel` locale 单测、5 locale key count 一致、i18n namespace tests、Evidence/Replay/Analysis/Review Markdown locale Rust 定向测试 | AgentRuntime projection 与 current Markdown 导出结构标题已本地化;legacy diagnostics 和正文级 copy 完整化不是本轮完成口径。 |
## 5. 关键测试用例覆盖
@@ -67,7 +67,7 @@ agent_runtime_submit_turn
| AR-ROUTE | done | routing single candidate / not possible / multi candidate decided 已有 stream 方法、projection 和 evidence tests。 |
| AR-ACTION / AR-POL | done | policy/action profile shape、tool hook real event 优先、ToolCall fallback 去重、permission signal coverage tests。 |
| AR-EVID / AR-EVID-LINK | done | `agent_runtime_export_evidence_pack` 输出 `runtime.json` / `artifacts.json` / observability correlationrequest telemetry 无匹配时输出空摘要。 |
| AR-UI-LINK | done | `AgentUI projection presentation 使用 i18n key` 已在 projection mapper、Team Workbench、Harness、Reliability、Artifact timeline 单测覆盖。 |
| AR-UI-LINK | done | `AgentUI projection presentation 使用 i18n key` 已在 projection mapper、Subagents、Harness、Reliability、Artifact timeline 单测覆盖。 |
| AR-CONTRACT | done | 本轮未新增命令边界;已有 `agent_runtime_*` current command contract 在既有 contract 测试口径内。 |
| AR-GUI | done/weak | `npm run verify:gui-smoke` 已覆盖 GUI 壳、DevBridge、workspace、runtime tool surface 等最小主路径;真实 tool approval 人机交互可继续补 Playwright 场景。 |
| AR-GOV | done/weak | current/deprecated/dead 分类已写入路线图;更强自动扫描守卫可在后续治理轮次继续收。 |
@@ -77,7 +77,7 @@
- 本地结构测试能用真实 runtime 输出构造 `submit-turn-event.json` 等价 fixture。
- 无 `session/thread/turn` 的 runtime 主路径事件不能进入 current read model。
- 前端未知事件控制器能识别 Lime Profile 事件为已知旁路事实,不污染用户可见 projection。
- AgentUI projection presentation mapper 支持用 key-based i18n 渲染 event type、phase/status、control、surfaceTeam Workbench、Harness、可靠性诊断和 Artifact timeline 这类 current projection 入口不得退回中文 fallback,不把 stable facts 翻译回写到 runtime/read model。
- AgentUI projection presentation mapper 支持用 key-based i18n 渲染 event type、phase/status、control、surfaceSubagents、Harness、可靠性诊断和 Artifact timeline 这类 current projection 入口不得退回中文 fallback,不把 stable facts 翻译回写到 runtime/read model。
- `turn.submitted / model.failed / task.failed` 等 profile fixtures 的 `type/status/failureCategory` 在不同 locale 下保持不变。
## 5. P2ThreadReadModel 与 Context 收口
+2 -2
View File
@@ -173,7 +173,7 @@ Owner 侧对应测试文档:
| AR-EVID-LINK-003 | completeness 按分类声明 | evidence export | 至少按 `runtime/telemetry/sources/claims/artifacts/verification/privacy/replay` 中适用分类输出状态 |
| AR-UI-LINK-001 | AgentUI 静默兼容 P1 profile event | `schemaVersion=lime-profile-0.4.0``type=turn.submitted` | unknown event controller 不告警,保持 stream 活跃 |
| AR-UI-LINK-002 | AgentUI 不把 profile event 写成 UI truth | dotted profile event stream | 只进入 runtime/read-model 投影或忽略;不得创建 `agentui_profile_store` / UI-only runtime status |
| AR-UI-LINK-003 | AgentUI projection presentation 使用 i18n key | Team Workbench / Harness / Reliability / Artifact timeline 的 projection event、phase、control、source、surface | `buildAgentUiTeamWorkbenchViewModel(..., { t })` 与 current projection 组件能输出 locale 文案;`type/status/phase/control/sourceType` stable facts 不被翻译回写 |
| AR-UI-LINK-003 | AgentUI projection presentation 使用 i18n key | Subagents / Harness / Reliability / Artifact timeline 的 projection event、phase、control、source、surface | `buildAgentUiSubagentsViewModel(..., { t })``AgentUiProjectionState.subagents``SubagentsView` 能输出 locale 文案;`type/status/phase/control/sourceType` stable facts 不被翻译回写 |
## 12. Contract / command mapping 用例
@@ -245,7 +245,7 @@ npm run governance:legacy-report
| 改 evidence/replay/review | Evidence Consistency 用例 + `npm run test:contracts` |
| 改 Workspace/Harness runtime 展示 | GUI smoke + ReadModel 用例 |
| 改 AgentContext / AgentPolicy / AgentEvidence linkage | 对应 AR-CTX / AR-POL / AR-EVID-LINK 用例 + 定向 owner ref 测试 |
| 改 AgentUI profile event 兼容 / presentation mapper | `agentStreamUnknownEventController` / `agentStreamListenerReadinessController` / `agentUiTeamWorkbenchViewModel` / `AgentUiTeamWorkbenchSurfaceView` / `TeamWorkbenchSummaryPanel` / `HarnessStatusPanel` / `AgentThreadReliabilityPanel` / `AgentThreadTimelineArtifactCard` 定向单测 |
| 改 AgentUI profile event 兼容 / presentation mapper | `agentStreamUnknownEventController` / `agentStreamListenerReadinessController` / `agentUiSubagentsViewModel` / `SubagentsView` / `HarnessStatusPanel` / `AgentThreadReliabilityPanel` / `AgentThreadTimelineArtifactCard` 定向单测 |
| 改 deprecated/compat 路径 | Governance guard + 相关 contract 用例 |
| 改 Rust runtime 主链 | App Server / RuntimeCore 定向 Rust 测试 + Runtime identity / Event 用例 + 必要时 `npm run verify:local` |
@@ -2,12 +2,12 @@
> 状态:v0.6.0 标准对齐完成;后续只跟踪 future boundary 与 v0.5 尾项增强
> 更新时间:2026-05-11
> 标准版本:Agent UI `v0.6.0`commit `dcf4bc5``Release Agent UI v0.6.0 team workbench`
> 范围:对齐 `/Users/coso/Documents/dev/ai/limecloud/agentui` v0.6.0、Claude Code、Codex、Vercel AI SDK、assistant-ui、LangGraph、OpenAI ChatKit/Apps SDK 等调研结论,并回挂 Lime 当前实现缺口。
> 标准版本:Agent UI `v0.6.0`commit `dcf4bc5`
> 范围:对齐 `/Users/coso/Documents/dev/ai/limecloud/agentui` v0.6.0、Claude Code、Vercel AI SDK、assistant-ui、LangGraph、OpenAI ChatKit/Apps SDK 等调研结论,并回挂 Lime 当前实现缺口。
## 1. 主目标
Lime AgentUI 的主目标不是只修复 thinking 样式,而是把对话工作台全流程收敛到一个可观察、可控制、可交付、可追溯的 projection 主链。v0.6.0 之后,主链范围从 `Conversation + Process + Task + Artifact + Evidence` 扩展到 `Team Workbench`,但 Lime 不因此新增第四类 runtime taxonomy。
Lime AgentUI 的主目标不是只修复 thinking 样式,而是把对话工作台全流程收敛到一个可观察、可控制、可交付、可追溯的 projection 主链。v0.6.0 之后,主链范围从 `Conversation + Process + Task + Artifact + Evidence` 扩展到 `Subagents`,但 Lime 不因此新增第四类 runtime taxonomy。
事实源声明:
@@ -15,7 +15,7 @@ Lime AgentUI 的主目标不是只修复 thinking 样式,而是把对话工作
Agent runtime event / subagent runtime event / automation job event / remote task event
-> Agent UI envelope projection
-> conversationProjectionStore.agentUi
-> Conversation / Inline Process / Task Capsule / Artifact / Evidence / Diagnostics / Team Workbench UI
-> Conversation / Inline Process / Task Capsule / Artifact / Evidence / Diagnostics / Subagents UI
```
固定规则:
@@ -24,7 +24,7 @@ Agent runtime event / subagent runtime event / automation job event / remote tas
2. Lime execution taxonomy 仍只承认 `agent turn``subagent turn``automation job` 三类一等执行实体。
3. Agent UI `runtimeEntity` 映射只能落到:`agent_turn``subagent_turn``automation_job`,以及 adapter 层的 `external_task``work_item`
4. `scheduler tick``execution run`、worker notification、review lane、remote heartbeat 都不能反向定义新的 Lime runtime taxonomy。
5. Team Workbench 是 projection surface,不是新的执行引擎。
5. Subagents 是 projection surface,不是新的执行引擎。
## 2. 标准引用与追溯页
@@ -32,11 +32,11 @@ Agent UI 标准引用统一落到专门页面,便于后续追溯,不在 Lime
| 类型 | 路径 / URL | 用途 |
| --- | --- | --- |
| v0.6.0 版本说明 | `/Users/coso/Documents/dev/ai/limecloud/agentui/docs/zh/versions/v0.6.0/overview.md` | 确认 Team Workbench、team event、runtime alignment 的版本边界。 |
| v0.6.0 版本说明 | `/Users/coso/Documents/dev/ai/limecloud/agentui/docs/zh/versions/v0.6.0/overview.md` | 确认 Subagents、team event、runtime alignment 的版本边界。 |
| v0.6.0 changelog | `/Users/coso/Documents/dev/ai/limecloud/agentui/docs/zh/versions/v0.6.0/changelog.md` | 追踪新增 surfaces、controls、event classes 与 schema 字段。 |
| v0.6.0 specification | `/Users/coso/Documents/dev/ai/limecloud/agentui/docs/zh/versions/v0.6.0/specification.md` | 规范级字段、事件与投影规则。 |
| 全流程 taxonomy | `/Users/coso/Documents/dev/ai/limecloud/agentui/docs/zh/reference/flow-and-taxonomy.md` | Conversation、Process、Task、Team、Artifact、Evidence、Diagnostics 的全流程分类。 |
| 来源索引 | `/Users/coso/Documents/dev/ai/limecloud/agentui/docs/zh/reference/source-index.md` | Claude Code、Codex、AI SDK、assistant-ui、LangGraph、ChatKit 等来源追溯。 |
| 来源索引 | `/Users/coso/Documents/dev/ai/limecloud/agentui/docs/zh/reference/source-index.md` | Claude Code、AI SDK、assistant-ui、LangGraph、ChatKit 等来源追溯。 |
| Runtime projection contract | `/Users/coso/Documents/dev/ai/limecloud/agentui/docs/zh/contracts/runtime-event-projection.md` | runtime source event 到 Agent UI envelope 的投影约束。 |
| AgentRuntime Profile | `/Users/coso/Documents/dev/ai/limecloud/agentruntime/docs/zh/specification.md``internal/roadmap/agentruntime/README.md` | Lime runtime facts 主链与 `lime-profile-0.4.0` profile event 边界。 |
| AgentContext | `/Users/coso/Documents/dev/ai/limecloud/agentcontext/docs/zh/specification.md` | context envelope、selection、budget、compaction、missing context 的 owner 边界。 |
@@ -57,14 +57,14 @@ Agent UI 标准已经从“thinking/progressive rendering 局部规则”扩展
| Tool lifecycle | input streaming、input available、running、progress、output available、output error 分阶段。 | 真实 source event 投影为 `tool.args.delta``tool.progress``tool.output.delta``tool.result``tool.failed`。 |
| HITL / action | approval、plan decision、structured input、clarification、permission grant 保持结构化,不从 assistant 文本反推。 | `action.required` / `action.resolved` 已进入 projectiondelegated approval 已补 plan approval 请求方 / 目标 session / 权限 / plan ref 结构化显示。 |
| Task / queue | foreground turn、queued steer、task capsule、background job 独立分类。 | queue/steer、`agent_turn/subagent_turn/automation_job` 已进入 projection`work_item` 通过 team formation / review fix / TaskUpdate owner change 接入;`external_task` 通过 remote task metadata 接入。 |
| Team Workbench surfaces | `team_roster``work_board``delegation_graph``handoff_lane``worker_notifications``review_lane``teammate_transcript``background_teammate``remote_teammate``team_policy`。 | 10 个标准 surface 已进入 summary、lane、surface detail、操作视图与 action target 定位;后续只补更深 teammate drilldown 和 future command boundary。 |
| Subagents surfaces | `team_roster``work_board``delegation_graph``handoff_lane``worker_notifications``review_lane``teammate_transcript``background_teammate``remote_teammate``team_policy`。 | 10 个标准 surface 已进入 summary、lane、surface detail、操作视图与 action target 定位;后续只补更深 teammate drilldown 和 future command boundary。 |
| Team topology | `coordinator_team``parallel_workers``specialist_handoff``review_team``human_agent_board``background_teammate``remote_teammate`。 | coordinator / parallel / specialist handoff / review / background / remote / human board 均已有 projection baseline;直接 board/team 写回、原生 A2A ingress、remote control callback 归入 future product boundary。 |
| Team events | `agent.spawned``agent.completed``agent.handoff``team.changed``worker.notification``review.requested``review.completed`。 | 标准 event class 与 subagent/handoff/review 基线已接入;reviewer teammate baseline 已进 rosterhandoff bundle / analysis handoff 导出已作为 runtime handoff source 以 `handoff_requested` 写入 `handoff_lane`accepted/returned/resumed 的 phase mapping 已在 adapter 层就绪,并已接入最小真实 source:`subagent_status_changed` 证明 accepted/returned`team_control_projection(action=resume)` 证明 resumedreassignment 与 surface 消费继续收口。 |
| Team controls | `delegate``assign``continue_agent``wait``stop``close``request_review`。 | 标准 controls 已有 source projection 与 UI 定位;本地 child session control 走真实 handler,非本地 target 只定位或回填 prompt,不伪造完成态。 |
| Runtime execution alignment | `runtimeEntity=agent_turn/subagent_turn/automation_job/external_task/work_item`,保留 parent/child、runtime status、team phase、queue/parallelism facts。 | Lime taxonomy 已吻合;本地三类 runtime 为一等实体,`external_task/work_item` 只作为 adapter surface,不反向新增 runtime taxonomy。 |
| Worker notification separation | worker result / failure / kill / usage / transcript ref 不能冒充用户消息或 final answer。 | terminal subagent status 与 archived subagent activity 已独立投影 `worker.notification`usage / duration / tool count / transcriptRef / result_ref 均来自结构化 source。 |
| Artifact / evidence | artifact body 离开 transcriptevidence/replay/review 走 durable refs。 | artifact/evidence/review/requested-fix execution metadata 已接入;artifact body 继续留在 artifact service/workbench。 |
| Context / permission / diagnostics | context budget、missing、compaction、policy、sandbox、safe diagnostics 独立分类。 | runtime context、permission、metrics 已有 projectionmemory budget / missing context 等细分 source event 属于 v0.5 尾项增强,不阻塞 v0.6 Team Workbench baseline。 |
| Context / permission / diagnostics | context budget、missing、compaction、policy、sandbox、safe diagnostics 独立分类。 | runtime context、permission、metrics 已有 projectionmemory budget / missing context 等细分 source event 属于 v0.5 尾项增强,不阻塞 v0.6 Subagents baseline。 |
### 3.1 AgentRuntime Profile 与相邻标准增补
@@ -73,7 +73,7 @@ Agent UI 标准已经从“thinking/progressive rendering 局部规则”扩展
```text
AgentRuntimeProfileEvent / ThreadReadModel / EvidenceSummary
-> Agent UI envelope projection
-> Workspace / Harness / Timeline / Task Capsule / Team Workbench
-> Workspace / Harness / Timeline / Task Capsule / Subagents
```
固定规则:
@@ -109,24 +109,24 @@ AgentRuntimeProfileEvent / ThreadReadModel / EvidenceSummary
| Permission/security/policy | `runtime_status.metadata` 的 permission / confirmation / profile / decision 投影为 `permission.changed`。 | `agentUiEventProjection.ts`、runtime action、permission UI | `current` + 待增强 |
| Diagnostics/metrics | warning/error/thread diagnostics、runtime cost/performance/routing/limit/quota 写入标准投影。 | `agentUiEventProjection.ts``conversationProjectionStore.ts``agentUiProjectionSummary.ts``HarnessStatusPanel.tsx``AgentThreadReliabilityPanel.tsx``agentStreamPerformanceMetrics.ts` | `current` |
### 4.2 v0.6.0 Team Workbench 再审计
### 4.2 v0.6.0 Subagents 再审计
| v0.6.0 要求 | Lime 当前事实 | 主要入口 | 结论 / 缺口 |
| --- | --- | --- | --- |
| `runtimeEntity` 映射 | Lime taxonomy 已定义 `agent turn` / `subagent turn` / `automation job`projection type 已显式携带 `runtimeEntity`。 | `internal/aiprompts/task-agent-taxonomy.md``agentUiEventProjection.ts``session_store.rs``subagent_runtime.rs``automation_service/*``remoteTaskAgentUiProjection.ts` | `current` + 待增强:本地 `agent_turn/subagent_turn/automation_job` 已接入;`work_item` 已通过 team formation -> work board baseline 写入;`external_task` 已从 gateway provenance -> `agent_runs.metadata` -> Team Workbench session-scoped projection baseline 接入。 |
| `runtimeEntity` 映射 | Lime taxonomy 已定义 `agent turn` / `subagent turn` / `automation job`projection type 已显式携带 `runtimeEntity`。 | `internal/aiprompts/task-agent-taxonomy.md``agentUiEventProjection.ts``session_store.rs``subagent_runtime.rs``automation_service/*``remoteTaskAgentUiProjection.ts` | `current` + 待增强:本地 `agent_turn/subagent_turn/automation_job` 已接入;`work_item` 已通过 team formation -> work board baseline 写入;`external_task` 已从 gateway provenance -> `agent_runs.metadata` -> Subagents session-scoped projection baseline 接入。 |
| Team queue / parallelism facts | 数据源存在:`team_phase``team_parallel_budget``team_active_count``team_queued_count``provider_concurrency_group``queue_reason`。 | `session_store.rs``teamWorkspaceRuntime.ts``hooks/useTeamWorkspaceRuntime.ts``liveRuntimeProjector.ts` | `current`:已进入 `run.status``team.changed``agent.changed``task.changed` envelopesurface 详情消费继续收口。 |
| Team Workbench surfaces | Lime 有 TeamWorkbench summary、team runtime cards、board canvas selectors、selected session detailsummary panel 已从 `conversationProjectionStore.agentUi` 消费 v0.6 全量 10 个标准 surface 的计数、latest events、三类 surface lane 与 10 个可展开 surface detaillane / surface 聚合已下沉为 `agentUiProjectionSummary.ts`,可交互 section / item / action / target 模型已下沉为 `agentUiTeamWorkbenchViewModel.ts`,并通过 `AgentUiTeamWorkbenchSurfaceView.tsx` 接入 panel;点击 `teammate_transcript/open_detail` 后已展示 Transcript Zoom 定位面,读取本地 child session 概览、live activity preview,并在没有 live activity 时按 child session 历史 activity snapshot 回填正文预览;Transcript Zoom 已从 child session detail 的 `queued_turns` 与 thread item `request_user_input` / `approval_request` / `tool_call` / message 类事件拆出输入队列、工具活动与近期消息,不从正文合成状态;点击工作台 action 后会按 agent / task / work item / transcript / artifact ref 串联同一目标的相关 projection 事件;`team_reassignment` 已在 surface detail 与操作视图中显示重新指派目标、负责人流转与原因;requested fix / worker notification 的 result ref、artifact ids / paths、evidence id 已进入 action target 与相关链路;pending requested fix work item 已可回填输入框,作为人工确认后的真实执行发起入口;也可在用户显式点击工作台 action 后通过 `submitAgentUiWorkbenchPrompt` 直接提交主线程 runtime turn,并在 request metadata 写入 `harness.agent_ui_workbench.action=execute_requested_fix`;回填 / 提交 prompt 都要求下一轮执行把结果写入 `metadata.requestedFixExecutionResults``artifact_snapshot` metadata 中的 `requestedFixExecutionResults` 已可即时回写 `work_board` 修复完成态;`work_board` source fact 已提供负责人 selector/callback,可回填 TaskUpdate owner 更新指令,并等待真实 `owner_change` source 确认;remote task source fact 已显示为“远端任务已定位”,避免 actual source baseline 仍被误报为“远端未接入”。 | `TeamWorkbenchSummaryPanel.tsx``AgentUiTeamWorkbenchSurfaceView.tsx``agentUiProjectionSummary.ts``agentUiTeamWorkbenchViewModel.ts``teamWorkspaceRuntime.ts``team-workspace-runtime/*` | `current` + 待增强:已有 scoped summary consumer、`Team 拓扑 / Worker 流 / Review-Handoff` lanes、native collapsible surface detail、可见操作视图、action target 定位闭环、related teammate projection chain、artifact/evidence/result ref 追溯、local child session action routing、reassignment 展示 baseline,以及 `teammate_transcript/open_detail` 结构化 source / zoom / live activity + 历史 activity snapshot + queued turn / thread item drilldown 预览;已补非本地 target route taxonomy 与可见状态(located_only / remote_task_source_located / unsupported_review / unsupported_handoff / unsupported_work_item / seeded_work_item / submitted_work_item / seeded_reassignment / work_item_source_located),并接入重指派 selector/callback 的 TaskUpdate 指令回填;更直接的 board/team API 写回已审计为 future command boundary;后续只在新增 current 命令 / shared task board service 后接入,不在 UI 本地伪造;无人工确认后台执行策略与更深 teammate 操作闭环继续排后。 |
| Subagents surfaces | Lime 已把 10 个标准 surface 收敛到 `AgentUiProjectionState.subagents``buildAgentUiSubagentsModel``agentUiSubagentsViewModel.ts``SubagentsView`projection summary selector 继续提供 surface / lane / latest event 聚合,产品侧只能消费这些读模型和 action callback。 | `packages/agent-ui-contracts/src/projection.ts``packages/agent-runtime-projection/src/subagents.ts``packages/agent-runtime-ui/src/subagents.tsx``src/components/agent/chat/projection/agentUiSubagentsViewModel.ts``agentUiProjectionSummary.ts``teamWorkspaceRuntime.ts``team-workspace-runtime/*` | `current` + 待增强:标准包已经承接 threads、delegation calls、activities、active/completed/failed thread ids 与 isolation 摘要;产品侧旧本地 panel / board 入口已退出 current 标准面。更直接的 board/team API 写回仍是 future command boundary;后续只在新增 current 命令 / shared task board service 后接入,不在 UI 本地伪造完成态。 |
| `agent.spawned` / `agent.completed` | subagent spawn/status/activity 和 child session 存储存在,标准 projection 已从 `subagent_status_changed` 派生 lifecycle。 | `subagent_runtime.rs``runtimeEventSubscriptions.ts``agentUiEventProjection.ts` | `current``agent.changed` 继续保留为 summary 兼容事件。 |
| `team.changed` | team phase、membership、queue/concurrency facts 在 runtime UI 有聚合;发送前 team formation state 已作为结构化 source 写入 Agent UI projection。 | `session_store.rs``teamWorkspaceRuntime.ts``liveRuntimeProjector.ts``agentUiEventProjection.ts``teamFormationAgentUiProjection.ts``useWorkspaceSendActions.ts` | `current` + 待增强:runtime status、subagent status 与 team formation 已派生 `team.changed`merging/done 的更细 source event 仍待增强。 |
| `worker.notification` | subagent activity、tool/task/action 事件已进入 projection storeterminal subagent status 与 archived subagent activity 已独立投影 worker notification`subagent_status_changed` parser / projection 已支持 `usage``duration_ms``tool_count``result_ref`terminal notification 顶层携带 `transcriptRef``workerUsage`Rust status emitter 已在 terminal status 输出 session usage、latest turn duration、tool count 与 latest AgentMessage runtime item `result_ref`Team Workbench view model 已把 worker usage / duration / tool count / result ref 展示到 subtitle / chips。 | `runtimeEventSubscriptions.ts``liveRuntimeProjector.ts``agentProtocol.ts``agentUiEventProjection.ts``agentUiTeamWorkbenchViewModel.ts``subagent_control.rs``subagent_runtime.rs` | `current` + 待增强:usage/duration/tool count 与 terminal `result_ref` 都已有真实 source`result_ref` 来自 latest turn 的 durable Agent runtime item ref,后续如需 artifact 级结果可再接 artifact source。 |
| `agent.handoff` | analysis handoff 与 runtime handoff bundle 导出已通过 evidence helper 以 `handoff_requested` 投影到 `handoff_lane`Team Workbench 已展示 status、from/to、resume target 与 context boundary`buildAgentUiHandoffProjectionEvents` 已按 source-provided status 映射 `accepted/acting/reconciling/completed/failed/cancelled/waiting` phaseplan approval、send input、wait/resume/close 有结构化控制。 | `HarnessStatusPanel.tsx``agentUiEventProjection.ts``agentUiProjectionSummary.ts``agentUiTeamWorkbenchViewModel.ts``subagent_runtime.rs``useWorkspaceTeamSessionControlRuntime.ts` | `current` + 待增强:runtime handoff bundle / analysis handoff 已有真实 `handoff_requested` source event`subagent_status_changed` 现在会把 child running/completed/failed/cancelled 映射为 specialist handoff accepted/returned/failed/cancelled`team_control_projection(action=resume)` 会在真实 affected session 返回后映射为 resumed;仍缺更独立的 handoff protocol callback 与外部 specialist lifecycle。 |
| `review.requested` / `review.completed` | Harness review decision 导出/保存已通过 review helper 投影到 `review_lane`;导出 review template 时也会记录 `team_control_projection(control=request_review)`,把人工审核请求作为 review work item 暴露给 Team Workbenchsurface detail 已展示 decision、reviewer、risk、checklist、followup、regression counts、requested fixes 与 regression requirement previewreview completed 的 reviewer 已额外进入 team roster`verification_summary` 的 failure/recovered facts 已回写到 review payload 和 requested-fix work item`team_control_projection(action=reassign)` 已能表达 work item reassignment source`TaskUpdate` owner 变更已输出结构化 `owner_change` 并由 `item_completed` 投影到 `work_board` assign/reassign。 | `HarnessStatusPanel.tsx``agentUiEventProjection.ts``teamControlAgentUiProjection.ts``agentUiProjectionSummary.ts`、evidence/review runtime | `current` + 待增强:reassignment adapter baseline 与 TaskUpdate owner_change board source 已有;requested fix execution 已能从 `artifact_snapshot` metadata 即时回写 `work_board`,并从 evidence artifact metadata 进入 verification summary 与 review decision;前端 reassignment selector/callback 已接入为 TaskUpdate owner 更新指令回填,负责人变化仍以真实 `owner_change` source 确认;requested fix 可回填输入框作为人工确认后的执行发起入口,也可在用户显式点击后直接提交 runtime turn,且 prompt 已声明 `requestedFixExecutionResults` metadata contractboard 完成态按 source-provided executionStatus 可见化,后台无确认自动执行仍不伪造。 |
| `worker.notification` | subagent activity、tool/task/action 事件已进入 projection storeterminal subagent status 与 archived subagent activity 已独立投影 worker notification`subagent_status_changed` parser / projection 已支持 `usage``duration_ms``tool_count``result_ref`terminal notification 顶层携带 `transcriptRef``workerUsage`Rust status emitter 已在 terminal status 输出 session usage、latest turn duration、tool count 与 latest AgentMessage runtime item `result_ref`Subagents view model 已把 worker usage / duration / tool count / result ref 展示到 subtitle / chips。 | `runtimeEventSubscriptions.ts``liveRuntimeProjector.ts``agentProtocol.ts``agentUiEventProjection.ts``agentUiSubagentsViewModel.ts``subagent_control.rs``subagent_runtime.rs` | `current` + 待增强:usage/duration/tool count 与 terminal `result_ref` 都已有真实 source`result_ref` 来自 latest turn 的 durable Agent runtime item ref,后续如需 artifact 级结果可再接 artifact source。 |
| `agent.handoff` | analysis handoff 与 runtime handoff bundle 导出已通过 evidence helper 以 `handoff_requested` 投影到 `handoff_lane`Subagents 已展示 status、from/to、resume target 与 context boundary`buildAgentUiHandoffProjectionEvents` 已按 source-provided status 映射 `accepted/acting/reconciling/completed/failed/cancelled/waiting` phaseplan approval、send input、wait/resume/close 有结构化控制。 | `HarnessStatusPanel.tsx``agentUiEventProjection.ts``agentUiProjectionSummary.ts``agentUiSubagentsViewModel.ts``subagent_runtime.rs``useWorkspaceTeamSessionControlRuntime.ts` | `current` + 待增强:runtime handoff bundle / analysis handoff 已有真实 `handoff_requested` source event`subagent_status_changed` 现在会把 child running/completed/failed/cancelled 映射为 specialist handoff accepted/returned/failed/cancelled`team_control_projection(action=resume)` 会在真实 affected session 返回后映射为 resumed;仍缺更独立的 handoff protocol callback 与外部 specialist lifecycle。 |
| `review.requested` / `review.completed` | Harness review decision 导出/保存已通过 review helper 投影到 `review_lane`;导出 review template 时也会记录 `team_control_projection(control=request_review)`,把人工审核请求作为 review work item 暴露给 Subagentssurface detail 已展示 decision、reviewer、risk、checklist、followup、regression counts、requested fixes 与 regression requirement previewreview completed 的 reviewer 已额外进入 team roster`verification_summary` 的 failure/recovered facts 已回写到 review payload 和 requested-fix work item`team_control_projection(action=reassign)` 已能表达 work item reassignment source`TaskUpdate` owner 变更已输出结构化 `owner_change` 并由 `item_completed` 投影到 `work_board` assign/reassign。 | `HarnessStatusPanel.tsx``agentUiEventProjection.ts``teamControlAgentUiProjection.ts``agentUiProjectionSummary.ts`、evidence/review runtime | `current` + 待增强:reassignment adapter baseline 与 TaskUpdate owner_change board source 已有;requested fix execution 已能从 `artifact_snapshot` metadata 即时回写 `work_board`,并从 evidence artifact metadata 进入 verification summary 与 review decision;前端 reassignment selector/callback 已接入为 TaskUpdate owner 更新指令回填,负责人变化仍以真实 `owner_change` source 确认;requested fix 可回填输入框作为人工确认后的执行发起入口;board 完成态按 source-provided executionStatus 可见化,后台无确认自动执行仍不伪造。 |
| `background_teammate` | durable automation job 是 current 执行实体;automation 创建/加载/更新/删除/立即运行已投影为 `runtimeEntity=automation_job``background_teammate``task_capsule` 与 terminal `worker.notification`。 | `lime-rs/src/services/automation_service/*``lime-rs/src/commands/automation_cmd.rs``src/lib/api/automation.ts``automationJobAgentUiProjection.ts``settings-v2/system/automation/index.tsx``WorkspaceRegisteredSkillsPanel.tsx` | `current` + 待增强:settings automation 与 capability drafts managed automation 入口已接持续刷新;仍可继续把 skills automation 等其它消费入口接入同一 helper。 |
| `remote_teammate` | current gateway ingressTelegram / Feishu / Discord / Wechat)已在 `agent.run` 传入 `source_metadata.remote_task`,包含 channel、account、remote task id、inbound message identity 与 Agent Card`rpc_handler.rs` 会把 snake/camel 两种 source metadata 持久化到 `agent_runs.metadata`terminal metadata 合并时保留 provenance`remoteTaskAgentUiProjection.ts` 已能从 `AgentRun.metadata.source_metadata.remote_task` 构造 `runtimeEntity=external_task` projection`TeamWorkbenchSummaryPanel` 通过 `useRemoteTaskExecutionRunProjection` 轮询当前 session 的 execution runs 并写入 `conversationProjectionStore.agentUi`。 | `internal/aiprompts/remote-runtime.md``channelsRuntime.ts``gateway_channel_cmd.rs``lime-rs/crates/gateway/src/*``rpc_handler.rs``remoteTaskAgentUiProjection.ts``useRemoteTaskExecutionRunProjection.ts``TeamWorkbenchSummaryPanel.tsx` | `current` + `partial`gateway remote ingress 已成为 Agent UI `remote_task_projection` actual source baselineterminal `AgentRun.status` 仍优先,且 `remote_task.event/taskStatus/status/state/phase` 已可驱动 A2A lifecyclequeued/running/needs_input/auth_required/completed/failed/cancelled),不从 heartbeat、gateway 日志文本、assistant prose 或普通 session message 猜测;`agent_runs.metadata.result.artifacts` / top-level `artifacts``remote_task.artifactUpdates` 与嵌套 `remote_task.task/a2aTask.artifacts` 已可在存在 `remote_task` provenance 时作为 remote artifact update source 投影,并保留 `contentRef/contentUrl/mimeType/byteSize/digest/preview` 等远端内容指针;Team Workbench remote surface 会展示内容指针、类型、校验与预览,且 URL 展示会去掉 query,避免把签名参数直接铺在 UI;`remote_task.inputRequired/authRequired/authStatus` 已可把 running run 投影为 `needs_input/auth_required` 并额外输出 `action.required(surface=remote_teammate, control=answer)`,但 terminal run 会压过 stale input/auth source;真正原生 A2A ingress 与 remote artifact store/content 回流已审计为 future product boundary;当前 v0.6 projection baseline 以 gateway provenance + structured remote_task metadata 为合规 source。 |
| `remote_teammate` | current gateway ingressTelegram / Feishu / Discord / Wechat)已在 `agent.run` 传入 `source_metadata.remote_task`,包含 channel、account、remote task id、inbound message identity 与 Agent Card`rpc_handler.rs` 会把 snake/camel 两种 source metadata 持久化到 `agent_runs.metadata`terminal metadata 合并时保留 provenance`remoteTaskAgentUiProjection.ts` 已能从 `AgentRun.metadata.source_metadata.remote_task` 构造 `runtimeEntity=external_task` projection并写入 `conversationProjectionStore.agentUi`。 | `internal/aiprompts/remote-runtime.md``channelsRuntime.ts``gateway_channel_cmd.rs``lime-rs/crates/gateway/src/*``rpc_handler.rs``remoteTaskAgentUiProjection.ts``useRemoteTaskExecutionRunProjection.ts` | `current` + `partial`gateway remote ingress 已成为 Agent UI `remote_task_projection` actual source baselineterminal `AgentRun.status` 仍优先,且 `remote_task.event/taskStatus/status/state/phase` 已可驱动 A2A lifecycle,不从 heartbeat、gateway 日志文本、assistant prose 或普通 session message 猜测;远端 artifact 指针继续保留为 refs;真正原生 A2A ingress 与 remote artifact store/content 回流已审计为 future product boundary。 |
| `work_board` / `assign` | Team board canvas 和 task capsule 有 UI 基础;发送前 `TeamWorkspaceRuntimeFormationState` 已记录 `team_formation_projection`,输出 `agent.changed(surface=team_roster/control=assign)``task.changed(surface=work_board/control=assign)``TaskUpdate` owner 变更会输出 `owner_change` metadata`item_completed` projection 会把初次 owner 写入映射为 `assign`,把 owner 切换映射为 `reassign`。 | `team-workspace-board/*``TaskCenterTabStrip.tsx``teamWorkspaceRuntime.ts``teamFormationAgentUiProjection.ts``useWorkspaceSendActions.ts` | `current` + 待增强:assignment / TaskUpdate owner_change reassignment baseline 不再依赖 runtime status hack 或 peer message 文本;已提供可见 reassignment selector/callback 的 TaskUpdate 指令回填;后续若要做到无 prompt 的直接 live board update,需先新增 current board/team 命令边界或 shared task board service;当前不从 UI 本地写状态,review work item 与完成态继续只认 source fact。 |
| Team controls | spawn/send/wait/resume/close 已有;stop 通过前台 stopSendingsubagent task projection 已把 `idle/queued/running/terminal` 映射为 `continue_agent/wait/stop/close``agent.spawned` 已携带 `delegate``team_control_projection` adapter 已覆盖 `delegate/assign/continue_agent/wait/stop/close/request_review` 标准 control,并可用 `action=reassign` + `control=assign` 表达 work item reassignment;当 source 提供 `resolvedStatus` 时会优先映射为 top-level `phase/runtimeStatus`,避免已知完成/失败/assigned 状态被固定显示成 queued/completedTeam formation 已作为 actual assignment source 投影 `assign/work_item`Harness review template 导出已作为 actual review request source 投影 `request_review/work_item`。 | `useWorkspaceTeamSessionControlRuntime.ts``teamControlAgentUiProjection.ts``teamFormationAgentUiProjection.ts``HarnessStatusPanel.tsx``agentUiEventProjection.ts``subagent_runtime.rs``agent_control.rs` | `current` + 待增强:现有 control 均已有实际来源;reviewer teammate baseline 已进入 rosterrequested fix execution artifact metadata 已进入 `work_board` 即时投影与 review decision sourcereassignment adapter、TaskUpdate owner_change source 与 source-provided `resolvedStatus` 映射已有;前端可见 reassignment selector/callback 已接入为 TaskUpdate 指令回填,真实负责人变化仍等 `owner_change` sourcerequested fixes 已可回填输入框作为人工确认后的执行发起入口,也可在用户显式点击后直接提交 runtime turn,且 prompt 已声明 `requestedFixExecutionResults` metadata contractboard 完成态已按 source-provided executionStatus 可见化,后台无确认自动执行仍不伪造。 |
| Delegated approval identity | team lead / teammate plan approval 已结构化,能发出 `action_resolved`direct `action_resolved(plan_approval)` projection 已保留 `targetSessionId``planFile``planId``awaitingLeaderApproval`summary/detail 已显示请求方、目标 session、权限模式、leader 审批等待与 plan ref,不只剩 metadata keys。 | `subagent_runtime.rs``agent_control.rs``runtimeEventSubscriptions.ts``agentUiEventProjection.ts``agentUiProjectionSummary.ts` | `current` + 待增强:后续如 runtime 提供更细 policy / transcript ref,可继续进入同一结构化详情,不从 prose 反推。 |
| Teammate transcript | child session / parent context 存在;`subagent_status_changed` 已派生 `agent.changed(surface=teammate_transcript, control=open_detail, transcriptRef=...)`summary consumer 与 view model 已能把 transcript ref 作为 action target 定位;Summary panel 已展示 Transcript Zoom 基线,解析 parent session、child session 与 latest turn,并通过 Team Workbench action routing 可落到本地 child sessionzoom 内已读取本地 child session 概览与 `liveActivityBySessionId` 中的最近进展;无 live activity 时会读取 child session 历史 activity snapshot 作为正文预览;zoom 内已用 child session detail 的 `queued_turns` thread item source type 拆出输入队列、结构化待输入/审批、工具活动与近期消息有界预览;已补 related teammate projection chain,按结构化 id 串联 transcript / worker notification / board / roster 等同目标事件。 | `agentUiEventProjection.ts``agentUiTeamWorkbenchViewModel.ts``TeamWorkbenchSummaryPanel.tsx``useWorkspaceCanvasSceneRuntime.tsx``teamWorkspaceRuntime.ts``activityPreviewSelectors.ts` | `current` + 待增强:transcript ref 事实源、target 定位、zoom 定位面、本地 child session 焦点、live activity preview、历史 activity snapshot fallback、queued turn / thread item 结构化 drilldown 与 related projection chain 已接入;仍需真实 pending input 操作 handler / 更深 transcript drilldown,且继续避免把 teammate 内部输出直接混进主 final answer。 |
| Teammate transcript | child session / parent context 存在;`subagent_status_changed` 已派生 `agent.changed(surface=teammate_transcript, control=open_detail, transcriptRef=...)`summary selector 与 Subagents view model 已能把 transcript ref 作为 action target 定位;本地 child session 焦点、live activity preview、历史 activity snapshot fallback 与 queued turn / thread item drilldown 均只消费结构化 fact,不从正文合成状态。 | `agentUiEventProjection.ts``agentUiSubagentsViewModel.ts``useWorkspaceCanvasSceneRuntime.tsx``teamWorkspaceRuntime.ts` | `current` + 待增强:transcript ref 事实源、target 定位、本地 child session 焦点、live activity preview、历史 activity snapshot fallback、queued turn / thread item 结构化 drilldown 与 related projection chain 已接入;仍需真实 pending input 操作 handler / 更深 transcript drilldown,且继续避免把 teammate 内部输出直接混进主 final answer。 |
## 5. Current / compat / deprecated / dead 分类
@@ -138,7 +138,7 @@ AgentRuntimeProfileEvent / ThreadReadModel / EvidenceSummary
| `threadItems` / `threadTurns` / `thread_read` timeline | `current` | 完成态过程归档、历史恢复、evidence 链路的主事实源。 | 无。继续演进。 |
| Inline active process | `current` | active turn 的首要过程投影;running 时优先于 timeline detail。 | 无。继续演进。 |
| Historical timeline archive | `current` | completed process 的归档投影;默认折叠。 | 无。继续演进。 |
| Team workspace runtime cards / live runtime projector | `current` | subagent/team runtime 的 current UI 读模型。 | 标准 surfaces 补齐后,作为 Team Workbench projection consumer。 |
| Subagents runtime cards / live runtime projector | `current` | subagent/team runtime 的 current UI 读模型。 | 标准 surfaces 补齐后,作为 Subagents projection consumer。 |
| Subagent runtime controls `spawn/send_input/wait/resume/close` | `current` | Lime teammate / child session 的 current 控制入口。 | 对齐标准 control 名称,补 `delegate/assign/continue_agent/wait/stop/close/request_review` 的 adapter。 |
| Automation job | `current` | durable background execution 的 current 执行实体;创建、更新、删除、立即运行、列表 / 状态 / run history 刷新已投影为 `runtimeEntity=automation_job``background_teammate``task_capsule` 与 terminal `worker.notification`。 | 后续其它 automation 消费入口继续接同一 projection helper;不得拆出新 runtime taxonomy。 |
| Remote task projection | `current` + actual source `partial` | `remoteTaskAgentUiProjection.ts``remote_task_projection` 的唯一 current builder;它输出 `agent.changed/task.changed(surface=remote_teammate, runtimeEntity=external_task)`、artifact refs 与 terminal `worker.notification``agentUiEventProjection.ts` 只保留兼容 wrapper。2026-05-10 已接入 gateway provenance:渠道 runtime 在 `agent.run` 写入 `source_metadata.remote_task`RPC terminal metadata 合并保留该 source,前端按 terminal `AgentRun.status`、结构化 remote task status 与 metadata 投影当前 session remote teammate。 | terminal `AgentRun.status` 仍优先;非 terminal run 可消费 `remote_task.event/taskStatus/status/state/phase` 这类结构化 remote task source,不得用 heartbeat、gateway 日志、assistant prose、普通 session message 或 teammate XML/JSON 文本反推。真正原生 A2A ingress、remote artifact store/content 回流与更完整远端任务详情归入 future product boundarycurrent baseline 继续只消费结构化 remote_task source。 |
@@ -148,7 +148,7 @@ AgentRuntimeProfileEvent / ThreadReadModel / EvidenceSummary
| Active turn leading timeline duplicate | `deprecated` | 与 inline process 同屏重复时必须被抑制。 | 保持只在没有 inline process fact 时作为 fallback。 |
| 从 assistant prose 推断 tool/artifact/success | `dead` | 与标准冲突;后续不得新增。 | 现有必要解析只能停留在 migration/fallback,并登记退出条件。 |
| 从 teammate XML/JSON 文本反推 worker result / approval | `dead` | v0.6 要求 worker notification 与 delegated approval 是结构化 fact。 | 只能保留在历史迁移或外部兼容边界;current 不再新增。 |
| `execution run` 作为 coordinator / task taxonomy | `dead` | `ExecutionTracker` 只能做观测层,不定义分工、调度或 parent/child 编排。 | 新设计不得把 `agent_runs` 当成 Team Workbench coordinator。 |
| `execution run` 作为 coordinator / task taxonomy | `dead` | `ExecutionTracker` 只能做观测层,不定义分工、调度或 parent/child 编排。 | 新设计不得把 `agent_runs` 当成 Subagents coordinator。 |
| Remote heartbeat 作为 terminal completion | `dead` | remote teammate 需要 remote truth / task status / artifact update,不得用 idle tick 猜完成。 | future remote adapter 必须保留 remote task id 和真实 terminal state。 |
## 6. 后续增强 / future boundary 清单
@@ -160,11 +160,11 @@ AgentRuntimeProfileEvent / ThreadReadModel / EvidenceSummary
| P0 | `runtimeEntity` 显式字段 / 类型 | 已可机械证明 `agent_turn/subagent_turn/automation_job` 进入 Agent UI envelope`work_item` 已有 team formation -> work board baseline`external_task` 已有 remote task projection helper。 | 继续禁止新建第四类 runtime taxonomyremote helper 只能消费真实 remote source。 |
| P0 | team queue / parallelism facts 进入标准 envelope | Team UI 与 Agent UI projection 已共享 team/provider queue facts。 | 继续补 surface 详情消费,避免各 UI 局部重算。 |
| P0 | `agent.spawned` / `agent.completed` 独立于 `agent.changed` | 已表达 teammate lifecycle、worker terminal result 与 parent/child lineage。 | 后续补更精确的 spawn source 与 transcript refs。 |
| P0 | `worker.notification` 独立 surface | terminal subagent status 与 archived subagent activity 已进入 `worker_notifications`,不进入主 transcript/final answerterminal status projection 已保留 `transcriptRef`Rust status emitter 已提供 usage / duration / tool count / result_refTeam Workbench 操作视图已显示这些 worker result facts。 | 已补 latest turn AgentMessage 的 durable runtime item `result_ref` source;后续如需 artifact 级结果再接 artifact source,仍不从 teammate 文本反推。 |
| P1 | Team Workbench surfaces consumer 基线已接入 | summary panel 已展示 roster、board、delegation、handoff、notifications、review、transcript、background、remote、policy 的 scoped counts、latest events、`Team 拓扑 / Worker 流 / Review-Handoff` lanes、可展开 surface detail 与 `工作台操作视图`;独立 view model 已保留 action target 与 attention 状态,并把 `review_requested_fix` work item 显式展示为可指派 Review fix,且能展示结构化 regression outcome`team_reassignment` work item 已显示重新指派目标、负责人流转、原因与 action labelpanel 已能把 action item 回传并在右侧定位标准 targetsubagent status 已提供 teammate transcript ref target`teammate_transcript/open_detail` 已有 Transcript Zoom 定位面,解析 child session / latest turn,并展示本地 child session 概览、live activity preview、历史 activity snapshot fallback、同目标 related projection chain 与 requested fix / worker result 的 artifact/evidence/result ref 追溯。 | 已补非本地 target route taxonomy、可见状态、requested fix 显式点击后的 runtime turn 提交与重指派 selector/callback 的 TaskUpdate 指令回填;直接 board/team API 写回已审计为 future command boundary,当前不作为 v0.6 projection 对齐阻塞项;后台无确认策略和更完整 teammate drilldown 排后。 |
| P1 | `agent.handoff` 基线已建模 | analysis handoff helper 与 handoff bundle 导出都已保留 from/to/reason/resume target/context boundary,并写入 `handoff_lane`Team Workbench surface detail / 操作视图已能显示 handoff status、from/to、resume target 与 context boundaryhandoff projection adapter 已把真实 source status 映射到标准 phase。 | 最小真实 source 已接:accepted/returned 来自 `subagent_status_changed`resumed 来自真实 resume control response;后续补更独立的 handoff protocol callback,不从 review decision 或 prose 合成。 |
| P0 | `worker.notification` 独立 surface | terminal subagent status 与 archived subagent activity 已进入 `worker_notifications`,不进入主 transcript/final answerterminal status projection 已保留 `transcriptRef`Rust status emitter 已提供 usage / duration / tool count / result_refSubagents 操作视图已显示这些 worker result facts。 | 已补 latest turn AgentMessage 的 durable runtime item `result_ref` source;后续如需 artifact 级结果再接 artifact source,仍不从 teammate 文本反推。 |
| P1 | Subagents surfaces consumer 基线已接入 | summary panel 已展示 roster、board、delegation、handoff、notifications、review、transcript、background、remote、policy 的 scoped counts、latest events、`Team 拓扑 / Worker 流 / Review-Handoff` lanes、可展开 surface detail 与 `工作台操作视图`;独立 view model 已保留 action target 与 attention 状态,并把 `review_requested_fix` work item 显式展示为可指派 Review fix,且能展示结构化 regression outcome`team_reassignment` work item 已显示重新指派目标、负责人流转、原因与 action labelpanel 已能把 action item 回传并在右侧定位标准 targetsubagent status 已提供 teammate transcript ref target`teammate_transcript/open_detail` 已有 Transcript Zoom 定位面,解析 child session / latest turn,并展示本地 child session 概览、live activity preview、历史 activity snapshot fallback、同目标 related projection chain 与 requested fix / worker result 的 artifact/evidence/result ref 追溯。 | 已补非本地 target route taxonomy、可见状态、requested fix 显式点击后的 runtime turn 提交与重指派 selector/callback 的 TaskUpdate 指令回填;直接 board/team API 写回已审计为 future command boundary,当前不作为 v0.6 projection 对齐阻塞项;后台无确认策略和更完整 teammate drilldown 排后。 |
| P1 | `agent.handoff` 基线已建模 | analysis handoff helper 与 handoff bundle 导出都已保留 from/to/reason/resume target/context boundary,并写入 `handoff_lane`Subagents surface detail / 操作视图已能显示 handoff status、from/to、resume target 与 context boundaryhandoff projection adapter 已把真实 source status 映射到标准 phase。 | 最小真实 source 已接:accepted/returned 来自 `subagent_status_changed`resumed 来自真实 resume control response;后续补更独立的 handoff protocol callback,不从 review decision 或 prose 合成。 |
| P1 | Review lane 基线已与 evidence 合流 | `review.requested` / `review.completed` 已进入 `review_lane` 并关联 evidence refsreview template 导出也会同步记录 `request_review/work_item` team control projectionsurface detail 已展示 reviewer、risk、checklist、followup、regression、requested fixes 与 regression requirement preview`review.completed` 已把 reviewer 投影为 `team_roster` reviewer teammaterequested fixes 已派生为 `work_board` work item,并在工作台操作视图中显示修复正文、pending 状态、regression outcome 和 assign target`team_control_projection(action=reassign)` 已能表达 reassignment source。 | requested fix execution artifact metadata 已进入 evidence / review decision 链路;TaskUpdate owner_change source 已接入 work_board;前端 reassignment selector/callback 已以 TaskUpdate 指令回填方式接入;当前已支持把 pending fix 回填输入框,由用户确认发送后执行,也支持用户显式点击后直接提交主线程 runtime turn,并要求后续写入 `requestedFixExecutionResults` metadataartifact snapshot 一旦带回该 metadata,会即时把 board 完成状态按 source-provided executionStatus 展示为“待执行修复 / 修复完成”等可见状态。 |
| P1 | Control 接线尾项 | `team_control_projection` adapter 已能表达 `delegate/assign/reassign/continue_agent/wait/stop/close/request_review`;现有 wait/resume/send_input/close/stop 已接 UI 操作且带 session scope`agent.spawned` 已作为 delegation source 携带 `delegate`Team formation 已把 actual assignment source 投影为 `assign/work_item`Harness review template 导出已把 actual request_review source 投影为 review work itemTeam Workbench action click 已只做标准 target 定位;无本地 child session 时会明确返回并显示 located_only / remote_task_source_located / unsupported_review / unsupported_handoff / unsupported_work_item / seeded_work_itempending requested fix 可回填输入框,也可在用户显式点击后提交 runtime turn;两条路径都不伪造运行时完成。 | 当前 control 来源已覆盖主路径;更细 review teammate 状态、无人工确认后台执行策略与无 prompt board/team API 写回都属于后续产品增强,直接写回需先补 current 命令边界。 |
| P1 | Control 接线尾项 | `team_control_projection` adapter 已能表达 `delegate/assign/reassign/continue_agent/wait/stop/close/request_review`;现有 wait/resume/send_input/close/stop 已接 UI 操作且带 session scope`agent.spawned` 已作为 delegation source 携带 `delegate`Team formation 已把 actual assignment source 投影为 `assign/work_item`Harness review template 导出已把 actual request_review source 投影为 review work itemSubagents action click 已只做标准 target 定位;无本地 child session 时会明确返回并显示 located_only / remote_task_source_located / unsupported_review / unsupported_handoff / unsupported_work_item / seeded_work_itempending requested fix 可回填输入框,也可在用户显式点击后提交 runtime turn;两条路径都不伪造运行时完成。 | 当前 control 来源已覆盖主路径;更细 review teammate 状态、无人工确认后台执行策略与无 prompt board/team API 写回都属于后续产品增强,直接写回需先补 current 命令边界。 |
| P2 | Background teammate 持续刷新 | settings automation 页刷新、创建、更新、删除、立即运行,以及 capability drafts managed automation 列表/开关已记录 `automation_job_projection`,并持续投影到 `background_teammate` / `task_capsule` / `worker_notifications`。 | 保持 automation job 为 current 执行实体;后续只需补 skills automation 等其它消费入口,不新增 runtime taxonomy。 |
| P2 | Remote teammate adapter | `remoteTaskAgentUiProjection.ts` 已提供 `external_task -> remote_teammate` adapter baseline,保留 Agent Card、remote task id、input/auth、artifact updates 与 terminal worker notificationgateway current ingress 已把真实 channel provenance / remote task id / Agent Card 通过 `agent_runs.metadata` 接入该 helper。 | 真正原生 A2A ingress、remote artifact store/content 回流、远端详情与控制 callback 已登记为 future product boundarycurrent baseline 不用假 remote surface、heartbeat completion 或普通文本反推状态。 |
| P2 | Context/memory 细分事件 | v0.5 尾项仍存在。 | memory budget、missing context、retrieval refs 结构化 source event。 |
@@ -172,55 +172,55 @@ AgentRuntimeProfileEvent / ThreadReadModel / EvidenceSummary
## 7. 下一刀排序
按对整体目标完成度的增量排序;若隔壁进程正在改 v0.6 Team Workbench / controls 文件,本进程只选择已确认非重叠的低冲突 projection 接线,不抢 Rust 高冲突实现文件或同一批 Team UI patch。
按对整体目标完成度的增量排序;若隔壁进程正在改 v0.6 Subagents / controls 文件,本进程只选择已确认非重叠的低冲突 projection 接线,不抢 Rust 高冲突实现文件或同一批 Team UI patch。
1. **深化 Team Workbench 专门视图**summary consumer、scoped lane selector、native collapsible surface detail、view model、可见 `工作台操作视图`、action target 定位、requested fixes work item + regression outcome 展示、local child session action routing、`teammate_transcript` source / Transcript Zoom / live activity + 历史 activity snapshot preview,以及 related teammate projection chain 已接入;非本地 target route taxonomy 与可见状态已补(含 work_board work item 未接入态、requested fix submitted_work_item、reassignment source fact 已定位态与 seeded_reassignment),直接 board/team API 写回已审计为 future command boundary;统一门禁已于 2026-05-10 fresh green;原生 A2A artifact content 已登记为 future product boundary,后台无确认策略和更完整 teammate drilldown 排后。
1. **深化 Subagents 专门视图**summary consumer、scoped lane selector、native collapsible surface detail、view model、可见 `工作台操作视图`、action target 定位、requested fixes work item + regression outcome 展示、local child session action routing、`teammate_transcript` source / Transcript Zoom / live activity + 历史 activity snapshot preview,以及 related teammate projection chain 已接入;非本地 target route taxonomy 与可见状态已补(含 work_board work item 未接入态、requested fix submitted_work_item、reassignment source fact 已定位态与 seeded_reassignment),直接 board/team API 写回已审计为 future command boundary;统一门禁已于 2026-05-10 fresh green;原生 A2A artifact content 已登记为 future product boundary,后台无确认策略和更完整 teammate drilldown 排后。
2. **补 Review lane 余量**review request 已有 actual sourcesurface detail 已展示 reviewer、risk、checklist、followup、regression、requested fixes 与 regression requirement previewreview 保存后的 `verification_summary` failure/recovered facts 已进入 review payload 与 requested-fix work item`team_control_projection(action=reassign)` 已能表达 reassignment sourceTaskUpdate owner_change 已作为真实 board source 投影到 work_boardpending requested fix 已支持显式点击后提交 runtime turn;无 prompt 的直接 board/team API 写回已降级为 future command boundary;统一门禁已收敛,后续只在新增 current board/team command 或 specialist handoff protocol 后继续接入;原生 A2A artifact content 已登记为 future product boundary。
3. **补其它 automation 消费入口**settings automation 与 capability drafts managed automation 已接持续刷新;后续如 skills automation 列表需要展示 background teammate,也接 `recordAutomationJobsAgentUiProjection`,不新增 runtime taxonomy。
4. **remote teammate / A2A source 接线**gateway actual source、remote task status/input/auth/artifact refs baseline 已有;真正原生 A2A ingress、remote artifact store/content 回流与远端控制 callback 已登记为 future product boundary,不用假 remote surface。
5. **v0.5 尾项回补**Context/memory 细分 source event、其他 provider tool input streaming、HITL/action 历史重放归档,排在 v0.6 Team Workbench 主缺口之后。
5. **v0.5 尾项回补**Context/memory 细分 source event、其他 provider tool input streaming、HITL/action 历史重放归档,排在 v0.6 Subagents 主缺口之后。
## 8. 2026-05-09 v0.6.0 再审计记录
- 已确认 Agent UI 标准版本为 `v0.6.0`local commit 为 `dcf4bc5`,公开 schema 为 `https://limecloud.github.io/agentui/schemas/agentui-event.schema.json`
- 已确认 Lime current taxonomy 只承认 `agent turn``subagent turn``automation job`,与 Agent UI `runtimeEntity` 的三类本地映射一致。
- 已确认 Lime 已有 subagent runtime、child session、team phase、queue/provider concurrency 数据源,主要缺口在标准 projection envelope 和 Team Workbench surface 消费。
- 已确认 Lime 已有 subagent runtime、child session、team phase、queue/provider concurrency 数据源,主要缺口在标准 projection envelope 和 Subagents surface 消费。
- 已确认 projection type 已补 v0.6 `runtimeEntity`、team controls 枚举与 `agent.spawned/agent.completed/team.changed/worker.notification/agent.handoff/review.*` event class;未完成项集中在 surface 详情消费、controls adapter、background/remote/board adapter。
- 已将旧下一刀从 Context/memory、provider tool streaming、HITL replay 调整为 v0.6 P0:先补 `runtimeEntity`、queue/parallelism facts、Team Workbench event classes。
- 已将旧下一刀从 Context/memory、provider tool streaming、HITL replay 调整为 v0.6 P0:先补 `runtimeEntity`、queue/parallelism facts、Subagents event classes。
- 已推进 Lime 实现增量:`runtime_status.metadata` 会投影 `runtimeEntity` 与 team queue/provider facts`subagent_status_changed` 会派生 `agent.spawned``agent.completed``team.changed``worker.notification`analysis handoff、runtime handoff bundle 与 review decision 已通过 helper 投影到 `agent.handoff``review.requested``review.completed`handoff 导出只标记 `handoff_requested`,表示交接包已生成但尚未被 specialist 接受。`agent.handoff` adapter 已按 source-provided status 映射 `accepted/acting/reconciling/completed/failed/cancelled/waiting` phaseaccepted/returned/resumed 已有最小 runtime/control source,仍不从 review decision、assistant prose、teammate transcript 或 heartbeat 合成。
- 已补 automation job projection 增量:SceneApp / Service Skill 创建自动化任务时记录 `automation_job_projection`,同时输出 `task.changed``agent.changed(surface=background_teammate)`terminal 结构化记录可输出 `worker.notification`surface selector 已支持按 `AgentUiSurface``conversationProjectionStore.agentUi` 读取。
- 已补 background teammate 持续刷新增量:`recordAutomationJobsAgentUiProjection` 支持批量记录 automation job 列表刷新;settings automation 页在刷新、创建、更新、删除、立即运行后会把 automation job 结构化记录到 Agent UI projection。
- 已补 capability drafts managed automation 增量:`WorkspaceRegisteredSkillsPanel` 加载 / 开关 managed automation job 时会记录 automation job projection,让 workspace skill 绑定视图里的后台 teammate 与 settings automation 共享同一事实源。
- 已补 Team Workbench surface consumer 增量:`TeamWorkbenchSummaryPanel` 通过 `useAgentUiProjectionEvents` 读取当前 session 的 Agent UI projection,并展示 `team_roster/work_board/delegation_graph/handoff_lane/worker_notifications/review_lane/teammate_transcript/background_teammate/remote_teammate/team_policy` 全量 surface 的计数与 latest events;对应回归覆盖 10 个 surface。
- 已补 Team Workbench scoped lane 增量:`agentUiProjectionSummary.ts` 现在提供可复用 lane selector,同一 summary panel`Team 拓扑``Worker 流``Review / Handoff` 三类聚合 latest events,避免 10 个 surface 只停留在扁平计数。
- 已补 Team Workbench surface detail 增量:`agentUiProjectionSummary.ts` 现在提供 10 个标准 surface 的 detail selector`TeamWorkbenchSummaryPanel` 展示 native collapsible `Surface 专门视图`,让每个 surface 可直接查看 latest events、phase 与 detail,而不是只能看总数。
- 已补 Team Workbench action 定位闭环:`agentUiTeamWorkbenchViewModel.ts` 将 10 个标准 surface 归一成 section / item / action / target / attention 模型,`AgentUiTeamWorkbenchSurfaceView.tsx` 将其渲染为 `工作台操作视图``TeamWorkbenchSummaryPanel.tsx` 现在接入 `onWorkbenchAction`,点击 action 会回传 item 并在右侧展示标准 target 明细,但不会从文本推断状态或伪造 remote / review / handoff 运行时调用。
- 已补 teammate transcript source 增量:`subagent_status_changed` 现在额外派生 `agent.changed(surface=teammate_transcript, control=open_detail)`,携带 `transcriptRef``agentUiTeamWorkbenchViewModel.ts``teammate_transcript` 优先使用 `transcriptRef` 作为 action target,保证 Team Workbench 可以定位队友 transcript,而不是把队友内部输出混入主 final answer。
- 已补 teammate transcript zoom 定位基线:`TeamWorkbenchSummaryPanel` 点击 `teammate_transcript/open_detail` 后展示 Transcript Zoom,显式列出 `transcriptRef`、父会话、子会话与最新回合;`useWorkspaceCanvasSceneRuntime.tsx` 的 action routing 会从 `childSessionId:turnId``parent/childSessionId` ref 解析本地 child session 焦点。该面板只做定位,不读取或复制 child transcript 正文到主 final answer。
- 已补 Subagents surface consumer 增量:`agentUiSubagentsViewModel.ts` 通过 `useAgentUiProjectionEvents` 读取当前 session 的 Agent UI projection,并支持展示 `team_roster/work_board/delegation_graph/handoff_lane/worker_notifications/review_lane/teammate_transcript/background_teammate/remote_teammate/team_policy` 全量 surface 的计数与 latest events;对应回归覆盖 10 个 surface。
- 已补 Subagents scoped lane 增量:`agentUiProjectionSummary.ts` 现在提供可复用 lane selector,同一 summary consumer`Team 拓扑``Worker 流``Review / Handoff` 三类聚合 latest events,避免 10 个 surface 只停留在扁平计数。
- 已补 Subagents surface detail 增量:`agentUiProjectionSummary.ts` 现在提供 10 个标准 surface 的 detail selector`SubagentsView` 展示 native collapsible `Surface 专门视图`,让每个 surface 可直接查看 latest events、phase 与 detail,而不是只能看总数。
- 已补 Subagents action 定位闭环:`agentUiSubagentsViewModel.ts` 将 10 个标准 surface 归一成 section / item / action / target / attention 模型,`SubagentsView` 将其渲染为 `工作台操作视图`;产品侧 action callback 点击后会回传 item 并在右侧展示标准 target 明细,但不会从文本推断状态或伪造 remote / review / handoff 运行时调用。
- 已补 teammate transcript source 增量:`subagent_status_changed` 现在额外派生 `agent.changed(surface=teammate_transcript, control=open_detail)`,携带 `transcriptRef``agentUiSubagentsViewModel.ts``teammate_transcript` 优先使用 `transcriptRef` 作为 action target,保证 Subagents 可以定位队友 transcript,而不是把队友内部输出混入主 final answer。
- 已补 teammate transcript zoom 定位基线:`SubagentsView` 点击 `teammate_transcript/open_detail` 后展示 Transcript Zoom,显式列出 `transcriptRef`、父会话、子会话与最新回合;`useWorkspaceCanvasSceneRuntime.tsx` 的 action routing 会从 `childSessionId:turnId``parent/childSessionId` ref 解析本地 child session 焦点。该面板只做定位,不读取或复制 child transcript 正文到主 final answer。
- 已补 teammate transcript live activity 预览基线:Transcript Zoom 在命中本地 child session 时展示子会话名称、状态、回合状态、角色、任务摘要,并读取 `liveActivityBySessionId[childSessionId]` 的最近进展;这不是从主 assistant prose 反推,也不会把 teammate 输出写入主 final answer。
- 已补 teammate transcript 历史正文 fallback:当 Transcript Zoom 命中本地 child session 但没有 live activity 时,`TeamWorkbenchSummaryPanel` 会用 `getAgentRuntimeSession(childSessionId, { historyLimit: 20 })` 读取子会话详情,并复用 `extractSessionActivitySnapshot` 生成最近 3 条历史 activity preview;该 preview 仍只展示在 teammate_transcript zoom 内,不写入主 final answer。
- 已补 selected teammate related projection chain`TeamWorkbenchSummaryPanel` 点击任一工作台 action 后,会按 agent / task / work item / review / handoff / remote task / transcript ref 精确匹配并展示同一目标的相关 projection 事件,用于把 roster、board、transcript、worker notification 串起来;该链路只读取结构化 Agent UI projection,不合成新的 runtime fact。
- 已补 teammate transcript 历史正文 fallback:当 Transcript Zoom 命中本地 child session 但没有 live activity 时,`SubagentsView` 会用 `getAgentRuntimeSession(childSessionId, { historyLimit: 20 })` 读取子会话详情,并复用 `extractSessionActivitySnapshot` 生成最近 3 条历史 activity preview;该 preview 仍只展示在 teammate_transcript zoom 内,不写入主 final answer。
- 已补 selected teammate related projection chain`SubagentsView` 点击任一工作台 action 后,会按 agent / task / work item / review / handoff / remote task / transcript ref 精确匹配并展示同一目标的相关 projection 事件,用于把 roster、board、transcript、worker notification 串起来;该链路只读取结构化 Agent UI projection,不合成新的 runtime fact。
- 已补 worker notification result facts 前端 adapter`agentProtocol.ts` 会解析 `subagent_status_changed``usage``duration_ms``tool_count``result_ref``agentUiEventProjection.ts` 在 terminal `worker.notification` 上保留 `transcriptRef``workerUsage` 和 payload 中的 duration / tool count / result ref。该增量只透传 source event 真值,不从 assistant prose 或 teammate transcript 反推。
- 已补 worker notification Rust source 增量:`SubagentRuntimeStatus` 只在 terminal status 上解析 session token usage、latest turn duration、latest turn tool count 与 latest AgentMessage runtime item `result_ref``SubagentStatusChangedEvent` 输出 `usage``duration_ms``tool_count``result_ref` 给前端 adapter`result_ref` 来自 runtime snapshot item,不从 teammate 文本反推。
- 已补 worker notification GUI baseline`agentUiTeamWorkbenchViewModel.ts` 会把 worker `transcriptRef``workerUsage`、duration、tool count 与 result ref 展示到操作视图 subtitle / chips;该展示仍只消费结构化 projection payload,不把 teammate 输出当作 worker result。
- 已补 Review lane 细状态展示:`formatAgentUiProjectionEventAuxiliaryDetail` 会把 review payload 中的 `decisionStatus``reviewer``riskLevel``checklistCount``followupActionCount``regressionRequirementCount` 转成 lane 辅助详情,并在 Team Workbench surface detail 中展示。
- 已补 Review requested fixes 明细:`buildAgentUiReviewProjectionEvents` 会保留 `requestedFixes``followupActions``regressionRequirements` 预览列表;`HarnessStatusPanel` 导出 / 保存 review decision 时把人工审核 followup actions 与 regression requirements 写入 projectionTeam Workbench surface detail 展示首条修复与回归要求。
- 已补 Review requested fixes work item 可见基线:`review.completed` 派生的 `task.changed(surface=work_board, runtimeEntity=work_item)` 现在会在 Team Workbench view model 中以修复正文作为 title,显示 `Review fix` / `pending` chip,并保留 `assign` control 与 review/work item target;这仍不代表修复已执行或已回写完成。
- 已补 Review requested fix regression facts 基线:`HarnessStatusPanel` 会把 review template / save 结果里的 `verification_summary.focus_verification_failure_outcomes``focus_verification_recovered_outcomes` 与 artifact validator outcome 转为 `regressionOutcome`,写入 `review.requested` / `review.completed` payload 和 requested-fix work itemTeam Workbench view model 会在修复项 subtitle / chip 显示 `回归:recovered|blocking_failure`。该增量只消费结构化 review/evidence fact,不把 pending fix 标成已执行。
- 已补 worker notification GUI baseline`agentUiSubagentsViewModel.ts` 会把 worker `transcriptRef``workerUsage`、duration、tool count 与 result ref 展示到操作视图 subtitle / chips;该展示仍只消费结构化 projection payload,不把 teammate 输出当作 worker result。
- 已补 Review lane 细状态展示:`formatAgentUiProjectionEventAuxiliaryDetail` 会把 review payload 中的 `decisionStatus``reviewer``riskLevel``checklistCount``followupActionCount``regressionRequirementCount` 转成 lane 辅助详情,并在 Subagents surface detail 中展示。
- 已补 Review requested fixes 明细:`buildAgentUiReviewProjectionEvents` 会保留 `requestedFixes``followupActions``regressionRequirements` 预览列表;`HarnessStatusPanel` 导出 / 保存 review decision 时把人工审核 followup actions 与 regression requirements 写入 projectionSubagents surface detail 展示首条修复与回归要求。
- 已补 Review requested fixes work item 可见基线:`review.completed` 派生的 `task.changed(surface=work_board, runtimeEntity=work_item)` 现在会在 Subagents view model 中以修复正文作为 title,显示 `Review fix` / `pending` chip,并保留 `assign` control 与 review/work item target;这仍不代表修复已执行或已回写完成。
- 已补 Review requested fix regression facts 基线:`HarnessStatusPanel` 会把 review template / save 结果里的 `verification_summary.focus_verification_failure_outcomes``focus_verification_recovered_outcomes` 与 artifact validator outcome 转为 `regressionOutcome`,写入 `review.requested` / `review.completed` payload 和 requested-fix work itemSubagents view model 会在修复项 subtitle / chip 显示 `回归:recovered|blocking_failure`。该增量只消费结构化 review/evidence fact,不把 pending fix 标成已执行。
- 已补 requested fix execution source`agentUiEventProjection.ts` 会从真实 `artifact_snapshot.metadata.requestedFixExecutionResults` 即时派生 `task.changed(surface=work_board, taskEvent=review_requested_fix)`,同时 `runtime_evidence_pack_service.rs``FileArtifact.metadata.requestedFixExecutionResults` / `requested_fix_execution_results` 收集执行结果,`runtime_review_decision_service.rs` 会把该结果带入 `verificationSummary.requestedFixExecutionResults` 并在 review decision Markdown 中显示 completed / recovered / blocked 统计与 `result_ref`;该链路只消费 artifact/evidence metadata,不从 review prose 或 teammate transcript 推断完成态。
- 已完成 requested fix regression facts 定向复验:3 个相关测试文件 / 64 个测试通过,相关 ESLint 通过,`npm run typecheck` 通过;`npm run verify:local``npm run verify:gui-smoke` 被隔壁 v0.6 Rust/protocol 并行变更阻断,失败点是 `AgentTokenUsage` 尚未实现 `PartialEq/Eq`,本轮为避免冲突未修改 Rust 高冲突文件。
- 已补 Review reviewer teammate 基线:`review.completed` 里存在 `reviewer` 时,`buildAgentUiReviewProjectionEvents` 会额外输出 `agent.changed(surface=team_roster, topology=review_team, runtimeEntity=work_item)`,保留 reviewer 名称、review id、decision/risk 与 `open_detail` target;该事件来自 review evidence fact,不从 prose 反推。
- 已完成 reviewer teammate 基线复验:`npm run verify:local` 通过,覆盖 app-version、lint、typecheck、Vitest smart 52 批、contracts、Rust `cargo test --manifest-path lime-rs/Cargo.toml` 与 GUI smoke。
- 已完成 requested fixes work item 展示增量复验:`npm run verify:local` 通过,覆盖 app-version、lint、typecheck、Vitest smart 52 批、contracts、Rust `cargo test --manifest-path lime-rs/Cargo.toml` 与 GUI smokeGUI smoke 跑通 workspace-ready、browser-runtime、site-adapters、agent-service-skill-entry、agent-runtime-tool-surface、agent-runtime-tool-surface-page、knowledge-gui、design-canvas。
- 已补 Team controls projection 基线:`subagent_status_changed -> task.changed` 会按 teammate status 给出 `continue_agent/wait/stop/close` 标准 control`agent.spawned` 会给出 `delegate``teamControlAgentUiProjection.ts` 已把 wait/resume/send_input/close/stop 等实际 Team 操作记录为 `team_control_projection`,输出 `team.changed(surface=team_policy)``task.changed(surface=work_board)`adapter 同时已支持 `delegate/assign/request_review`,分别落到 `delegation_graph/work_board/review_lane`,并优先消费 source-provided `resolvedStatus` 作为 top-level phase/runtimeStatus。
- 已补 Team Workbench 非本地 target route taxonomy 与可见状态:`routeAgentUiTeamWorkbenchAction` 在命中本地 child session 时继续接 `continue_agent/wait/close/stop` 真实 handler;没有本地 child session 时按标准 target 返回 `located_only``remote_task_source_located``unsupported_review``unsupported_handoff``unsupported_work_item`pending requested fix 可返回 `seeded_work_item` 并回填输入框,`team_reassignment` source fact 可返回 `work_item_source_located`,负责人 selector/callback 可返回 `seeded_reassignment` 并回填 TaskUpdate 指令;`TeamWorkbenchSummaryPanel` 会展示“只定位 / 远端任务已定位 / 审核未接入 / 交接未接入 / 工作项未接入 / 已回填输入 / 重指派已回填”等状态,让 UI 保持定位可追溯,同时不伪造 remote / review / handoff runtime 调用,也不伪造 work_board 完成态。
- 已补 requested fix work board 状态可见化:`agentUiTeamWorkbenchViewModel.ts` 会把 source-provided `executionStatus` 显示为“待执行修复 / 修复完成 / 修复失败”等状态,并把 pending assign 标为“指派修复”、completed open_detail 标为“查看修复结果”;该增量只消费 review/evidence fact,不伪造修复执行。
- 已补 requested fix 执行发起入口 baseline`routeAgentUiTeamWorkbenchAction` 对 pending `review_requested_fix` work item 可生成修复 prompt 并通过 `seedAgentUiWorkbenchPrompt` 回填输入框,返回 `seeded_work_item`;发送仍由用户确认触发,不自动伪造执行中或完成态。
- 已补 Subagents 非本地 target route taxonomy 与可见状态:产品侧 Subagents action callback 在命中本地 child session 时继续接 `continue_agent/wait/close/stop` 真实 handler;没有本地 child session 时按标准 target 返回 `located_only``remote_task_source_located``unsupported_review``unsupported_handoff``unsupported_work_item`pending requested fix 可返回 `seeded_work_item` 并回填输入框,`team_reassignment` source fact 可返回 `work_item_source_located`,负责人 selector/callback 可返回 `seeded_reassignment` 并回填 TaskUpdate 指令;`SubagentsView` 会展示“只定位 / 远端任务已定位 / 审核未接入 / 交接未接入 / 工作项未接入 / 已回填输入 / 重指派已回填”等状态,让 UI 保持定位可追溯,同时不伪造 remote / review / handoff runtime 调用,也不伪造 work_board 完成态。
- 已补 requested fix work board 状态可见化:`agentUiSubagentsViewModel.ts` 会把 source-provided `executionStatus` 显示为“待执行修复 / 修复完成 / 修复失败”等状态,并把 pending assign 标为“指派修复”、completed open_detail 标为“查看修复结果”;该增量只消费 review/evidence fact,不伪造修复执行。
- 已补 requested fix 执行发起入口 baseline产品侧 Subagents action callback 对 pending `review_requested_fix` work item 可生成修复 prompt 并回填输入框,返回 `seeded_work_item`;发送仍由用户确认触发,不自动伪造执行中或完成态。
- 已补 requested fix seed prompt 的 execution result metadata contractpending fix 回填输入框时会要求后续执行把 `requestedFixExecutionResults` 写入 evidence/artifact metadata,字段覆盖 requestedFix、executionStatus、regressionOutcome、resultRef 与 artifactIds/artifactPaths`artifact_snapshot` 带回该 metadata 后会即时进入 `work_board`,该增量只消费真实 source 回写契约,不伪造完成态。
- 已补 Team formation assignment source:发送前生成本地 Team formation 时,会通过 `teamFormationAgentUiProjection.ts` 记录 `team.changed(surface=team_roster)``agent.changed(surface=team_roster/control=assign)``task.changed(surface=work_board/control=assign)`Team control 操作也已携带当前 `sessionId`,避免 summary panel 过滤不到 wait/resume/close/stop 事件。
- 已补 TaskUpdate owner_change reassignment source`TaskUpdateTool` 在 owner 真正变化时输出结构化 `ownerChange` / `owner_change``agentUiEventProjection.ts` 只从 `item_completed + tool_call(TaskUpdate) + metadata.updated_fields includes owner` 派生 `work_board` assign/reassign,保留 previous/next assignee 与 source tool/task list id,不从 `task_assignment` peer message、assistant prose 或 teammate transcript 反推负责人变化。
- 已补 Review request actual source`HarnessStatusPanel` 导出人工审核模板时,除了 `review.requested` evidence event,还会记录 `team_control_projection(control=request_review)`,以 `review_relative_root` 作为 review work item,落到 `review_lane` 并保留 `sessionId/threadId` scope。
- 已补 Remote teammate actual source baseline`remoteTaskAgentUiProjection.ts` 提供 `remote_task_projection`,将结构化 remote task 映射为 `agent.changed/task.changed(surface=remote_teammate, runtimeEntity=external_task)`,保留 Agent Card、remote task id、input/auth、artifact refs,并且只在真实 terminal status 下输出 `worker.notification`,避免 heartbeat 猜完成;input/auth need 会额外输出 `action.required(surface=remote_teammate, control=answer)`,让远端待输入 / 待鉴权成为结构化 HITL fact;当 `remote_task` provenance 提供 `event/taskStatus/status/state/phase` 或嵌套 `task/a2aTask` artifact updates 时也会进入同一 projectionterminal `AgentRun.status` 仍优先压过 stale remote/input/auth source`agentUiEventProjection.ts` 内的旧 remote teammate builder 已改为兼容 wrapper,统一委托该 helper。2026-05-10 已把 `gateway_channel_*` / gateway crate 接到 current source:渠道 runtime 写入 `source_metadata.remote_task``agent_runs.metadata` 保留 provenance`useRemoteTaskExecutionRunProjection` 再按当前 session 投影到 Team Workbench;剩余原生 A2A ingress 与 remote artifact store/content 回流已登记为 future product boundary。
- 已补 Remote teammate actual source baseline`remoteTaskAgentUiProjection.ts` 提供 `remote_task_projection`,将结构化 remote task 映射为 `agent.changed/task.changed(surface=remote_teammate, runtimeEntity=external_task)`,保留 Agent Card、remote task id、input/auth、artifact refs,并且只在真实 terminal status 下输出 `worker.notification`,避免 heartbeat 猜完成;input/auth need 会额外输出 `action.required(surface=remote_teammate, control=answer)`,让远端待输入 / 待鉴权成为结构化 HITL fact;当 `remote_task` provenance 提供 `event/taskStatus/status/state/phase` 或嵌套 `task/a2aTask` artifact updates 时也会进入同一 projectionterminal `AgentRun.status` 仍优先压过 stale remote/input/auth source`agentUiEventProjection.ts` 内的旧 remote teammate builder 已改为兼容 wrapper,统一委托该 helper。2026-05-10 已把 `gateway_channel_*` / gateway crate 接到 current source:渠道 runtime 写入 `source_metadata.remote_task``agent_runs.metadata` 保留 provenance`useRemoteTaskExecutionRunProjection` 再按当前 session 投影到 Subagents;剩余原生 A2A ingress 与 remote artifact store/content 回流已登记为 future product boundary。
- 已补 active stream overlay 收口:`text_delta` 继续走低频 overlay 实时显示,`final_done` 后回填到 message content/contentParts;失败态保留已有 tool/process contentParts 并追加失败正文,避免“无最终正文”错误把工具过程清空。
- 已完成 2026-05-10 final gate fresh 复验:`npm run verify:local` 统一门禁通过,覆盖 app-version、lint、typecheck、Vitest smart 52 批、contracts、Rust 全量与 GUI smoke;此前旧 stale gate / DevBridge 不可用问题已由 fresh unified gate 证据替代,不再阻塞 v0.6.0 标准对齐完成判定。
@@ -230,21 +230,21 @@ AgentRuntimeProfileEvent / ThreadReadModel / EvidenceSummary
- 前端与契约门禁:`npm run verify:app-version` 通过(版本 `1.32.0`),`npm run lint` 通过,`npm run typecheck` 通过,`npm test` 通过(Vitest smart 52 批),`npm run test:contracts` 通过,`npm run check:agent-runtime-clients` 通过,`npm run harness:doc-freshness` clean。
- Rust 门禁:`npm run verify:local` 内的 `cargo test --manifest-path lime-rs/Cargo.toml` 通过;修复本地 `lime-rs/target` 构建缓存后,单独复核 `cargo test --manifest-path "lime-rs/Cargo.toml"` 也通过:`lime` root unit tests 1295 passed`src/main.rs` 0 passedintegration tests 2 passed,真实联网测试 2 ignored。
- GUI 门禁:`npm run verify:local` 内的 `npm run verify:gui-smoke` 通过;覆盖 DevBridge health、前端壳、workspace-ready、browser-runtime、site-adapters、agent-service-skill-entry、agent-runtime-tool-surface、agent-runtime-tool-surface-page、knowledge-gui、design-canvas,并完成本轮 smoke Chrome profile 清理。
- 完成判定:Agent UI v0.6.0 projection / Team Workbench / review lane / work_board / worker notification / remote teammate baseline 已达到可交付门槛。`direct board/team write-back``native A2A ingress``remote artifact store/content bytes``remote control callback`、无人工确认后台执行策略与更深 teammate drilldown 均保持 future product / command boundary,不作为 v0.6.0 完成阻塞项。
- 完成判定:Agent UI v0.6.0 projection / Subagents / review lane / work_board / worker notification / remote teammate baseline 已达到可交付门槛。`direct board/team write-back``native A2A ingress``remote artifact store/content bytes``remote control callback`、无人工确认后台执行策略与更深 teammate drilldown 均保持 future product / command boundary,不作为 v0.6.0 完成阻塞项。
## 8.2 2026-05-10 Board/team 直接写回命令边界审计
- 已审计 current 命令边界:`src/lib/api`、App Server / Electron Desktop Host 命令边界、`agentCommandCatalog.json``mockPriorityCommands.ts``defaultMocks` 均没有可复用的 board/team 直接写回命令;`TaskCreate/TaskList/TaskGet/TaskUpdate` 当前是 aster runtime tool / inventory,不是 legacy desktop facade command。
- 已确认真实可写事实源仍是 `TaskUpdateTool`:它通过 `resolve_task_board_state` / `persist_task_board_state` 读写 session task board,并在 owner 变化时输出 `ownerChange` / `owner_change` metadataAgent UI 只消费该结构化 metadata 派生 `work_board` assign/reassign。
- 已明确不新增临时 UI 本地写回或平行 legacy desktop command。若未来要做无 prompt direct board/team write-back,必须先新增 App Server current method 或 shared task board service,并同步前端网关、Electron / App Server 命令边界、治理目录册与测试 mock,同时解决 runtime `shared_task_list_storage` 与 session `extension_data` 的一致性。
- 因此 v0.6 标准对齐口径调整为:`work_board` / reassignment 的合规 baseline 是 `Team Workbench selector -> TaskUpdate prompt/runtime turn -> TaskUpdateTool owner_change source -> Agent UI projection`;无 prompt 直接写回归入 future product command boundary,不再作为本轮 projection 对齐阻塞项。
- 因此 v0.6 标准对齐口径调整为:`work_board` / reassignment 的合规 baseline 是 `Subagents selector -> TaskUpdate prompt/runtime turn -> TaskUpdateTool owner_change source -> Agent UI projection`;无 prompt 直接写回归入 future product command boundary,不再作为本轮 projection 对齐阻塞项。
## 8.3 2026-05-10 Playwright E2E 与真实 child context 复验
- 已补浏览器 DevBridge subagent control 命令族:`agent_runtime_spawn_subagent``agent_runtime_send_subagent_input``agent_runtime_wait_subagents``agent_runtime_resume_subagent``agent_runtime_close_subagent` 已接入 dispatcher 与 HTTP cooldown bypass,契约仍落在现有 `agent_runtime_*` current 主链;后续不得在 `lime-rs/src/commands/**` 追加新业务逻辑。
- 已补 session store limited history 空会话探测:`get_runtime_session_detail_with_history_page` 不再因为 persisted empty fast path 跳过 runtime overlay / subagent context`agent_runtime_get_session(parent, { historyLimit })` 能返回 `child_subagent_sessions`,空 child detail 也能返回 `subagent_parent_context`
- Playwright E2E 复验已覆盖:普通会话 tab 不显示 `AgentUI N` 内部 badgeTeam Workbench `Agent UI v0.6 / 10 events`、10 个 surface、三类 lane、`工作台操作视图``Surface 专门视图`、Harness AgentUI 投影、真实 child context 与 console `0 error / 0 warning` 均有证据。
- 证据索引:`internal/exec-plans/agentui-playwright-e2e-2026-05-10.md`;截图与 JSON 位于 `internal/exec-plans/evidence/agentui-e2e-2026-05-10/09-session-store-subagent-context.json``09-team-workbench-real-child-context-after-session-store-fix.png``10-transcript-open-detail-real-child-focus-context.png``10-console-errors-final-real-child-focus.txt`
- Playwright E2E 复验已覆盖:普通会话 tab 不显示 `AgentUI N` 内部 badgeSubagents `Agent UI v0.6 / 10 events`、10 个 surface、三类 lane、`工作台操作视图``Surface 专门视图`、Harness AgentUI 投影、真实 child context 与 console `0 error / 0 warning` 均有证据。
- 证据索引:`internal/exec-plans/agentui-playwright-e2e-2026-05-10.md`;截图与 JSON 位于 `internal/exec-plans/evidence/agentui-e2e-2026-05-10/`,覆盖 session store subagent context、真实 child context、transcript detail focus 与 console final check
- 复验命令:`cargo test --manifest-path "lime-rs/Cargo.toml" -p lime-agent should_probe -- --nocapture``cargo test --manifest-path "lime-rs/Cargo.toml" dev_bridge --lib -- --nocapture`、相关 Vitest / ESLint、`npm run typecheck``npm run test:contracts``npm run verify:gui-smoke -- --reuse-running` 均通过。
- 已补 child focus `返回主助手` 点击稳定性:embedded Team board sticky header 提升到 `z-40`,返回按钮增加独立可点击层级与 `data-testid`Playwright harness 证据位于 `internal/exec-plans/evidence/product-backend-ux-e2e-2026-05-10/01-child-focus-return-parent-before-click.png``02-return-parent-after-click.png``02-return-parent-hit-target.json``02-console-errors-return-parent.txt`,点击前命中目标为 `BUTTON`,点击后返回按钮消失,console `0 error / 0 warning`
- 剩余项继续降级为 E2E 深度增强:让真实 child turn 产出最小 activity preview、避免 requested-fix action 在测试中误触发真实 runtime turn、在 DevBridge `3030` 恢复后用真实 child session 再复跑同一返回路径;这些不阻塞 v0.6.0 标准 baseline 完成判定。
@@ -272,8 +272,8 @@ AgentRuntimeProfileEvent / ThreadReadModel / EvidenceSummary
- 已补 subagent stream 投影入口:`runtimeEventSubscriptions.ts` 将 team workspace 的 `subagent_status_changed``agent_subagent_stream:<session>` 写入 `conversationProjectionStore.agentUi`
- 已补 automation job 投影入口:`buildAgentUiAutomationJobProjectionEvents``recordAutomationJobAgentUiProjection` 将结构化 automation job record 投影为 `automation_job` runtime entity,并接入 SceneApp / Service Skill 自动化创建路径。
- 已补 projection 消费收口:`agentUiProjectionSummary.ts``useConversationProjectionStore.ts``TaskCenterTabStrip``AgentThreadTimelineArtifactCard``AgentThreadReliabilityPanel``HarnessStatusPanel` 已开始按 scoped selector 消费标准投影。
- 已补相关回归测试:adapter、store、runtime handler、provider tool delta、MCP notification、hydration、metrics、Harness evidence、TaskCenter、Artifact、Diagnostics、team runtime subscription、v0.6 Team Workbench surface summary 均已有定向覆盖;下一步重点补 controls adapter 与 richer lane 级 UI 回归。
- 已补相关回归测试:adapter、store、runtime handler、provider tool delta、MCP notification、hydration、metrics、Harness evidence、TaskCenter、Artifact、Diagnostics、team runtime subscription、v0.6 Subagents surface summary 均已有定向覆盖;下一步重点补 controls adapter 与 richer lane 级 UI 回归。
## 10. 与路线图主线关系
这份文件继续服务 `internal/roadmap/agentui/README.md` 的主目标:AgentUI 不是聊天页,而是 `Conversation + Process + Task + Artifact + Evidence + Team Workbench` 工作台。Thinking UI 的修复只是 P0 体感缺口;v0.6.0 的 flow/taxonomy 用于约束后续每一刀都回到同一条 current projection 主链。
这份文件继续服务 `internal/roadmap/agentui/README.md` 的主目标:AgentUI 不是聊天页,而是 `Conversation + Process + Task + Artifact + Evidence + Subagents` 工作台。Thinking UI 的修复只是 P0 体感缺口;v0.6.0 的 flow/taxonomy 用于约束后续每一刀都回到同一条 current projection 主链。
+114
View File
@@ -0,0 +1,114 @@
# Lime Agent Workbench 路线图
> 状态:active
> 更新时间:2026-06-11
> 范围:`/Users/coso/Documents/dev/ai/limecloud/lime-agent-workbench` 文档站、Lime AgentUI SDK 标准、AgentRuntime 配合边界、Subagents 标准化。
## 主目标
`lime-agent-workbench` 从“参考 AG-UI 的中文文档站”推进成 Lime 内部 AgentUI / AgentRuntime 标准指引,并让后续实现只能向 Lime current 主链收敛:
```text
App Server / RuntimeCore / ExecutionBackend
-> RuntimeEvent / ThreadReadModel / TaskSnapshot / EvidencePack
-> @limecloud/agent-ui-contracts
-> @limecloud/agent-runtime-projection
-> @limecloud/agent-runtime-ui
-> Product Apps / Content Studio / Agent Apps
```
Workbench 不是外部 SDK 套壳,也不是第二套 runtime。外部 AG-UI、AI SDK、OpenAI Agents JS、LangGraph 等只能作为参考;Lime 的事实源仍是 App Server、RuntimeCore、ExecutionBackend、AgentUI Projection、Evidence / Replay / Review 和治理分类。
## 固定结论
1. AgentRuntime 拥有执行事实;AgentUI 只消费 facts 并投影 UI。
2. UI 标准使用 `UIMessageParts``ProcessTimeline``ExecutionGraph`、ToolGroup、ActionRequired、ArtifactRef、EvidenceRef;不再使用非标准组件树术语。
3. TypeScript 包线拆为四个 current 包,`@limecloud/agent-ui` 只允许作为 future facade。
4. 产品应用不能各自复制过程组件、runtime client、投影 reducer 或工具状态机。
5. `subagents` 必须进入 RuntimeEvent / TaskSnapshot / ExecutionGraph / `AgentUiProjectionState.subagents` 主链,不能靠 assistant 正文或本地 React state 表达。
6. 文档站必须和 Lime 现有代码、路线图、App Server / AgentRuntime / AgentUI 事实源对齐;不能写成外部标准翻译稿。
## 当前阶段
当前处于 `v0.2.x -> v0.4.x` 标准化阶段:
| 阶段 | 状态 | 目标 |
| --- | --- | --- |
| v0.1.0 | done | 建站、发布 GitHub Pages、形成中文文档骨架。 |
| v0.2.0 | done | 去掉非标准树形过程方向,初步对齐 AG-UI / AI SDK / Lime runtime 现实。 |
| v0.3.0 | active | 补齐 Subagents、SDK 包边界、runtime client、projection conformance、Lime 现有实现迁移路线。 |
| v0.4.0 | active | 从文档标准推进到 Lime 主仓包实现、guard、Artifact/Evidence refs surface、Agent App 真实 smoke 和 Content Studio 标准 UI surface adoption。 |
## 阅读顺序
| 文档 | 作用 |
| --- | --- |
| [iteration-plan.md](iteration-plan.md) | 本次迭代目标、缺口、写集和完成判定。 |
| [task-board.md](task-board.md) | 当前迭代可并行认领的任务板、写集、验收和推荐下一刀。 |
| [parallel-workstreams.md](parallel-workstreams.md) | 多进程并行认领方式、每条 workstream 的写集和验收。 |
| [acceptance.md](acceptance.md) | Workbench 标准、SDK、runtime、subagents 的统一验收矩阵。 |
| [standard-package-release-runbook.md](standard-package-release-runbook.md) | 标准 npm 包发布、Content Studio 安装接入和 GUI smoke 的阻塞解除步骤。 |
相关既有路线图:
- `internal/roadmap/agentui/README.md`
- `internal/roadmap/agentruntime/README.md`
- `internal/roadmap/agentruntime/agentui-adoption-gap.md`
- `internal/roadmap/agentruntime/agentruntime-standard-adoption-gap.md`
- `internal/roadmap/agentapp/README.md`
## current / compat / deprecated / dead
### current
- `lime-agent-workbench` GitHub Pages 中文标准站。
- Lime App Server JSON-RPC + RuntimeCore + ExecutionBackend runtime facts。
- `RuntimeEvent / ThreadReadModel / TaskSnapshot / EvidencePack`
- `@limecloud/agent-ui-contracts``@limecloud/agent-runtime-projection``@limecloud/agent-runtime-ui``@limecloud/agent-runtime-client`
- Product Apps 通过 runtime client + projection + React surfaces 复用 AgentUI。
- Agent App 运行页通过标准 `AgentRuntimeClient``projectAgentUiState``AgentUiProjectionView` 接入共享 SDK seed。
- Agent App Host action 决策已走 current `AgentRuntimeClient.respondAction -> agentSession/action/respond`Host drawer 主体只保留抽屉、指标和标准 projection 接线,本地 process fallback / projection 输入增强已拆成独立模块。
- Agent App 真实 Electron 主路径已通过 smoke`Agent Apps` 聚合入口 -> runtime iframe SDK -> App Server current `agentSession/*` -> Host Agent Run 标准 projection DOM。
- Content Studio `AI agents` 工作台和通用 `AgentSessionPanel` 已统一通过 `AgentUiProjectionSurface` 消费共享 projection read model,并输出 `.agent-ui-projection` / `.agent-ui-main` / `.agent-ui-sidecar` 标准 DOM surface;其 standalone/dev App Server turn 主链已通过 `@limecloud/agent-runtime-client/sessionGateway` 包装为标准 `AgentRuntimeClient`。标准 npm 链路已发布并接入 `@limecloud/app-server-client@1.66.0``@limecloud/agent-runtime-client@0.1.1`Content Studio 不能安装误发的无 scope 链路。
- `@limecloud/agent-runtime-ui` 已有 `ArtifactRefList` / `EvidenceRefList`,引用内容由宿主 artifact workspace / evidence pack owner 打开。
- `@limecloud/agent-runtime-ui` 已有 `SubagentsView`,只消费 `state.subagents`
### compat
- Lime 现有 `packages/app-server-client` 作为 `@limecloud/app-server-client` 的本地开发事实源。
- 未来 `@limecloud/agent-ui` facade 作为非 current 可选聚合入口。
- 产品应用本地 `messages / executionEvents / process panel / host bridge service` 作为迁移缓存;已接入共享 SDK 的页面不得再新增平级本地过程事实源。
- Agent App task event 作为 projection 输入,但必须桥接到标准 event class。
退出条件:facade 明确不导出 runtime transport,产品应用只显式依赖 current 四包后,不再用规划名描述 current owner。
### deprecated
- 每个产品应用自建 Agent 过程组件。
- 从 assistant prose 解析工具成功、artifact 类型、evidence verdict。
- 把 UI component tree 当成 runtime protocol。
- AgentUI React 组件直接订阅 Provider 或读取 App Server DB。
### dead
- 非标准树形过程术语作为 Lime 标准术语或协议。
- 已清退的旧协作 UI surface 作为 current 类型、组件、state 字段或 SDK API。
- 第二套 Agent App runtime facts。
- 业务 App 直连 Provider API 作为生产 Agent 能力。
- 把外部 SDK 作为 Lime runtime owner 直接套用。
## 本轮下一刀
优先补 Lime 主仓与 `lime-agent-workbench` 之间的实现闭环:
1. 补 Content Studio GUI smoke / 产品回归,证明 `.agent-ui-projection` 标准 surface 在真实页面中可运行,并把边界审计纳入常规验证。
2. Agent App projection bridge 已拆成 normalization facade、projection builders、field readers、runtime event adapter 和 mapping helpers;下一步只在标准 projection 空态覆盖足够后下线本地 process fallback。
3. 继续收紧治理 guard,防止非标准树术语、本地 process owner、直连 Provider runtime 和第二套 runtime facts 回流。
## 并行协作规则
多进程并行时,先只读 `git status --short`,再认领窄写集。默认不要多个进程同时编辑同一个文件;如果必须改同一导航文件,先完成内容页,再由一个进程统一接导航。
本路线图目录是并行协作入口。新进程先读 `task-board.md` 认领窄写集,完成后把进度写回 `iteration-plan.md` 的进度日志,避免聊天记录成为唯一状态源。
发布与 Content Studio 接入阶段先读 `standard-package-release-runbook.md`。当前 scoped registry 发布与 Content Studio 依赖接入已完成;后续提升完成度前必须先补 GUI smoke / 产品回归证据。
@@ -0,0 +1,122 @@
# Agent Workbench 验收矩阵
> 状态:active
> 范围:文档站标准、SDK 边界、AgentRuntime 配合、Subagents、治理与发布。
## 1. 文档站验收
| 项 | 必须满足 | 证据 |
| --- | --- | --- |
| 架构 | App Server、RuntimeCore、ExecutionBackend、AgentUI Projection、Product Apps 的关系清楚 | `docs/concepts/architecture.md` |
| 协议参考 | AG-UI、AI SDK、OpenAI Agents JS 等只作为 reference,不成为 Lime owner | `docs/agentic-protocols.md` |
| 标准术语 | 使用 UIMessageParts、ProcessTimeline、ExecutionGraph、ToolGroup、ActionRequired、ArtifactRef、EvidenceRef | `docs/contracts/ui-projection.md` |
| 禁止旧词 | 不把非标准树形过程术语写成 Lime 标准 | 全站搜索 |
| 导航 | 新增核心页面能从 sidebar 访问 | `docs/.vitepress/config.mts` |
| 构建 | VitePress build 通过 | `npm run docs:build` |
## 2. SDK 验收
| 包 | current 职责 | 禁止 |
| --- | --- | --- |
| `@limecloud/agent-ui-contracts` | types、schemas、fixtures、version constants | React、transport、provider、产品应用依赖 |
| `@limecloud/agent-runtime-projection` | projector、selectors、hydration、replay、reconciliation、Subagents model | React DOM、Electron、Provider SDK、App Server DB |
| `@limecloud/agent-runtime-ui` | React primitives、surfaces、callbacks、i18n label contract、CSS / DOM contract | runtime truth、Provider key、JSON-RPC transport |
| `@limecloud/agent-runtime-client` | App Server JSON-RPC、Host bridge、events、read APIs、action response、evidence export | UI projection state、React component、Provider Store owner |
| `@limecloud/agent-ui` | future facade,只 re-export UI 侧包 | runtime transport、业务实现 |
最小 API 验收:
- contracts 有 RuntimeEvent、ThreadReadModel、TaskSnapshot、ProjectionState、ArtifactRef、EvidenceRef、AgentUiSubagentsModel。
- projection 能 `projectAgentUiState``createAgentUiProjector``replayAgentUiFixture``replayAppServerFacts``buildAgentUiSubagentsModel`
- react 组件只接受 projection state 和 command callbacksSubagents 只消费 `state.subagents`Artifact/Evidence refs 只展示轻量 ref 并通过 callback 交还宿主。
- runtime-client 有 `startTurn``subscribeEvents``nextEvent``readThread``respondAction``cancelTurn``exportEvidence`
## 3. Runtime 配合验收
| 能力 | 必须事实化 | UI 表达 |
| --- | --- | --- |
| turn 生命周期 | `turn.submitted / started / completed / failed` | Runtime status、message shell |
| 模型输出 | `model.delta / completed / failed` | UIMessageParts text |
| reasoning / plan | `reasoning.delta / summary``plan.delta / final` | reasoning part、ProcessTimeline |
| 工具 | `tool.started / args / progress / result / failed` | ToolGroup、ExecutionGraph step |
| 人工动作 | `action.required / resolved / cancelled / expired` | ActionRequired |
| artifact | `artifact.changed / versioned / exported` | ArtifactRef、artifact workspace |
| evidence | `evidence.changed / exported / review.verdict` | Evidence lane、review/replay |
| subagent | `subagent.started / updated / completed / failed` | Subagents threads、ExecutionGraph child |
| handoff / review | `handoff.requested / completed``review.verdict` | Subagents delegation / activity summary |
| 断流恢复 | `stream.repaired / snapshot.updated` | stale / repairing 状态 |
不合格情况:
- 只有 assistant prose,没有 typed payload。
- UI 从本地 state 推断工具成功或 evidence verdict。
- 产品应用直读 Provider key 或 App Server DB。
- Runtime provider 输出 React props。
## 4. Subagents 验收
| 项 | 必须满足 |
| --- | --- |
| identity | subagent 必须有 `subagentId`,并关联 parent task / run / step。 |
| lineage | parent/child、handoff、dependency edges 可被 ExecutionGraph 表达。 |
| lifecycle | started、progress、blocked、completed、failed、cancelled 可区分。 |
| communication | channel message、worker notification、handoff request 不伪装成普通用户消息。 |
| control | cancel / pause / resume / respond action 进入 runtime control plane。 |
| evidence | 子代理输出能进入 artifact/evidence refs,保留 correlation ids。 |
| projection | `AgentUiProjectionState.subagents` 有 threads、delegationCalls、activities、active/completed/failed thread ids 和 isolation 摘要。 |
| degradation | 缺 lineage 时显示 unknown / unavailable,不猜测团队结构。 |
## 5. Conformance Fixture 验收
每个 runtime provider 或 SDK 切片至少准备以下 fixture
| Fixture | 覆盖 |
| --- | --- |
| text-basic | streaming delta + final reconciliation |
| tool-success | tool args/progress/result + output ref |
| tool-failure | failure category + recovery action |
| hitl-action | action required/resolved + waiting state |
| artifact-evidence | artifact changed + evidence exported |
| stream-repair | interrupted stream + read model repair |
| subagent-handoff | parent task -> child subagent -> handoff/review |
通过标准:
- Projection replay 幂等。
- 重放顺序由 `sequence` 和 scope ids 决定。
- 大输出只通过 refs 表达。
- 断流后能从 read model 修复。
## 6. Lime 主仓迁移验收
| 现有路径 | 目标 | 验收 |
| --- | --- | --- |
| `packages/app-server-client` | `@limecloud/agent-runtime-client` seed | 不生成 UI projection,不带 React。 |
| `packages/agent-runtime-projection` | `@limecloud/agent-runtime-projection` current | projector / selectors 在 React 外可测,输出 `state.subagents`。 |
| `packages/agent-runtime-ui` | `@limecloud/agent-runtime-ui` current | 只消费 projection stateSubagents 不重算 runtime factsArtifact/Evidence refs 不读取大 payload。 |
| `packages/agent-app-runtime/projection` | compat adapter | 迁完后不保留第二套 projection owner。 |
| 产品应用 local process component | deprecated | 迁到共享 ProcessTimeline / ExecutionGraph / Subagents surfaces。 |
## 7. 发布验收
| 项 | 命令 / 证据 |
| --- | --- |
| 构建 | `npm run docs:build` |
| 版本 | `package.json``package-lock.json` 一致 |
| 更新记录 | `docs/development/updates.md` |
| GitHub Pages | `.github/workflows/deploy.yml` |
| tag / push | 仅在用户明确要求后执行 |
## 8. 完成度口径
| 完成度 | 口径 |
| --- | --- |
| 30% | 文档站骨架和核心概念存在。 |
| 50% | P0 文档齐全,构建通过,导航完整。 |
| 70% | SDK API 草案、fixture、conformance 和迁移表完整。 |
| 85% | Lime 主仓 seed 包实现可被至少一个产品应用消费。 |
| 95% | 四包 seed、SDK reference、refs surface、Agent App seed adoption 和 Content Studio adoption blueprint 完成,但仍缺真实产品主路径 smoke。 |
| 97% | Agent App 主路径已通过真实 Electron smoke 证明复用 runtime-client + projection + React surfacesContent Studio 仍停留在 adoption blueprint。 |
| 98% | Content Studio 产品 UI 主路径已统一到标准 projection surface,且有边界审计阻止页面直接拼共享 primitives;但 App Server turn 主链仍未抽成标准 session gateway 形状。 |
| 98.5% | Content Studio App Server turn 主链已抽到标准 session gateway 形状,`nextEvent()` 保持 `agentSession/event` notification 合同,sidecar service 已委托 gateway;标准包已发布到 `@limecloud` organization`@limecloud/app-server-client@1.66.0``@limecloud/agent-runtime-client@0.1.1`。Content Studio 已固定安装 registry 版 `@limecloud/agent-runtime-client@0.1.1`,并通过 `@limecloud/agent-runtime-client/sessionGateway` 包装现有 gateway;首轮误发的无 scope `app-server-client@1.66.0` 和依赖旧名的 `@limecloud/agent-runtime-client@0.1.0` 只作为 compat-misrelease 记录。GUI smoke 仍未补齐,因此不提升到 100%。 |
| 100% | Content Studio 主路径真实复用 runtime-client + projection + React surfaces,且 Agent App / Content Studio 均有 smoke / contract 证明。 |
@@ -0,0 +1,217 @@
# Agent Workbench v0.3.0 迭代计划
> 状态:active
> 创建时间:2026-06-10
> 目标仓库:`/Users/coso/Documents/dev/ai/limecloud/lime-agent-workbench`
> Lime 规则归档:`internal/roadmap/agentworkbench/`
## 主目标
把 Agent Workbench 做成 Lime AgentUI / AgentRuntime 标准指引,能指导 Content Studio、Agent Apps 和后续 Lime 主仓 SDK 收敛实现,而不是只提供概念介绍。
本次迭代完成后,应能回答:
1. Lime AgentUI 标准表面有哪些,哪些是 current owner。
2. `@limecloud/agent-ui-contracts``@limecloud/agent-runtime-projection``@limecloud/agent-runtime-ui``@limecloud/agent-runtime-client` 如何拆包,为什么不合成一个大包。
3. App Server / RuntimeCore / ExecutionBackend 需要输出哪些 factsAgentUI 如何消费。
4. Subagents 在 Lime 中怎么建模、投影、验收。
5. 现有 Lime 包和产品应用如何迁移,哪些路径是 compat / deprecated / dead。
## 当前事实
| 事实 | 状态 |
| --- | --- |
| `lime-agent-workbench` 已有 VitePress 文档站和 GitHub Pages workflow | done |
| `package.json` 版本为 `0.2.0` | done |
| 文档已有 architecture、runtime-event、ui-projection、quickstart、SDK overview | done |
| 文档已经去除非标准树形过程术语作为标准方向 | done |
| `/subagents` 顶层页缺失 | done |
| SDK 四包已从文档规划推进到 package-level API / fixture / conformance seed | done |
| runtime-client 与 App Server Host 配合仍偏概念 | done |
| Workbench 与 Lime 主仓现有包的迁移关系需要更明确 | partial |
| Content Studio UI 主路径已统一到标准 AgentUI surface | partial |
## 写集边界
本轮推荐写集:
```text
/Users/coso/Documents/dev/ai/limecloud/lime-agent-workbench/docs/**
/Users/coso/Documents/dev/ai/limecloud/lime-agent-workbench/docs/.vitepress/config.mts
/Users/coso/Documents/dev/ai/limecloud/lime-agent-workbench/README.md
/Users/coso/Documents/dev/ai/aiclientproxy/lime/internal/roadmap/agentworkbench/**
```
谨慎写集:
```text
/Users/coso/Documents/dev/ai/aiclientproxy/lime/packages/agent-runtime-client/**
/Users/coso/Documents/dev/ai/aiclientproxy/lime/packages/agent-ui-contracts/**
/Users/coso/Documents/dev/ai/aiclientproxy/lime/packages/agent-runtime-projection/**
/Users/coso/Documents/dev/ai/aiclientproxy/lime/packages/agent-runtime-ui/**
```
谨慎写集需要先盘点当前 dirty diff,避免覆盖其他进程工作。
本轮禁止顺手写集:
```text
/Users/coso/Documents/dev/ai/aiclientproxy/lime/src/features/agent-app/**
/Users/coso/Documents/dev/ai/aiclientproxy/lime/src/lib/dev-bridge/**
/Users/coso/Documents/dev/ai/aiclientproxy/lime/lime-rs/**
```
除非另起实现分支并明确认领,否则不要在本次文档标准化任务里夹写这些生产代码。
## P0 任务
### P0.1 Subagents 标准页
目标:新增 `/subagents` 顶层页,固定 Lime 对 subagent、worker、task、job、channel、handoff、team review 的标准表达。
必须覆盖:
- Runtime owner`task.created / subagent.started / channel.message / handoff.requested / review.verdict` 等事件家族。
- UI projectionExecutionGraph、Subagents threads、delegation calls、activity summary、Artifact / Evidence refs。
- 控制面:spawn、cancel、pause、resume、handoff、respond_action。
- 降级:solo run、missing lineage、unavailable remote channel。
- 禁止:assistant prose 代表子代理完成、本地 UI state 自建 worker truth。
验收:
- `docs/subagents.md` 存在,并在 nav/sidebar 中可访问。
- 页面明确 `current / compat / deprecated / dead`
- 页面链接到 Runtime 事件、读模型、UI 投影、Agents 概念页。
### P0.2 SDK 四包标准页补强
目标:让 TypeScript SDK 文档能直接指导 Lime 主仓包实现。
必须覆盖:
- `@limecloud/agent-ui-contracts`types、schemas、fixtures、versioning。
- `@limecloud/agent-runtime-projection`projector、selectors、hydration、reconciliation、replay、Subagents model。
- `@limecloud/agent-runtime-ui`React primitives、surfaces、callbacks、i18n label contract、CSS / DOM contract。
- `@limecloud/agent-runtime-client`JSON-RPC、Host bridge、event subscription、read APIs、errors。
- `@limecloud/agent-ui`future optional facade,只 re-export UI 侧,不带 transport。
验收:
- SDK 章节至少新增 React surfaces / conformance / package boundaries 页面,或等价补全现有页面。
- 文档明确禁止依赖方向。
- 文档给出最小 API 草案和 fixture replay 示例。
### P0.3 Runtime 配合页补强
目标:把 AgentRuntime 与 AgentUI 的配合写成实现合同,而不是概念图。
必须覆盖:
- RuntimeEvent envelope 与 scope id。
- ThreadReadModel / TaskSnapshot 的 hydration 责任。
- App Server Host JSON-RPC 方法分组。
- Event stream repair、snapshot reconciliation、large output refs。
- Provider Store、Tool inventory、Policy / Permission / Sandbox owner。
验收:
- `contracts/app-server-host.md``contracts/runtime-read-model.md``quickstart/runtime-provider.md` 至少有一处明确端到端实现路径。
- Runtime provider 文档强调不依赖 React、不生成 UI props。
### P0.4 构建和发布前验证
目标:保证 GitHub Pages 可发布。
必须运行:
```bash
npm run docs:build
```
可选运行:
```bash
npm run docs:dev
```
验收:
- VitePress build 通过。
- 新增页面无断链或导航缺页。
- 若版本号变化,必须同步 `package.json``package-lock.json` 和 release 文档。
## P1 任务
| 任务 | 目标 |
| --- | --- |
| Workbench vs Lime 主仓迁移表 | 把现有 packages 与目标四包逐项映射,给出退出条件。 |
| Conformance fixture 设计 | done:定义纯文本、工具成功、工具失败、HITL、artifact/evidence、断流恢复、subagent handoff fixture,并落到 `@limecloud/agent-ui-contracts`。 |
| Content Studio 接入蓝图 | 把 Content Studio 从产品页面角度接入 runtime-client + projection + react surfaces。 |
| Governance guard 设计 | 后续在 Lime 主仓加扫描,防止非标准树形过程术语 / 本地 process component / provider direct API 回流。 |
| 多进程任务板 | done:把 runtime-client、App Server facts runner、governance guard、product adoption、React surfaces、release integration 拆成可认领 workstream。 |
## P2 任务
| 任务 | 目标 |
| --- | --- |
| SDK seed 实现 | 在 Lime 主仓 packages 中把 contracts/projection/react/runtime-client seed 变成可消费包。 |
| Runtime provider fixture runner | done`@limecloud/agent-runtime-projection` 已能 replay 标准 contracts fixtures,并能消费 App Server current read / event / evidence export facts。 |
| AgentUI React smoke | done`@limecloud/agent-runtime-ui` 已用标准 fixture replay state 渲染 MessageParts、ProcessTimeline、ExecutionGraph、ActionRequired、Subagents。 |
| GitHub Pages 版本发布 | 按版本标签发布 `v0.3.0` 或后续版本。 |
## 进度日志
| 时间 | 进度 | 证据 |
| --- | --- | --- |
| 2026-06-10 | 建立 `internal/roadmap/agentworkbench` 迭代入口,固定主目标、写集、P0/P1/P2。 | 本文件、`README.md``parallel-workstreams.md``acceptance.md`。 |
| 2026-06-10 | 完成 Workbench P0 文档标准化:新增 Subagents、TypeScript package boundaries、React surfaces、Conformance,补强 App Server Host、Runtime read model、Runtime provider,并接入导航。 | `docs/subagents.md``docs/sdk/typescript/package-boundaries.md``docs/sdk/typescript/react-surfaces.md``docs/sdk/typescript/conformance.md``docs/.vitepress/config.mts`。 |
| 2026-06-10 | 完成发布前构建验证。 | `npm run docs:build` 通过;仅有 VitePress chunk size warning,非阻塞。 |
| 2026-06-10 | 完成 Lime 主仓 SDK contracts seed`@limecloud/agent-ui-contracts` 新增标准 conformance fixtures、runtime/read model/projection/fixture validation helpers,并扩展 subagent/handoff/review scope ids。 | `packages/agent-ui-contracts/src/fixtures.ts``packages/agent-ui-contracts/src/validation.ts``npm --prefix packages/agent-ui-contracts run test` 通过。 |
| 2026-06-10 | 完成 projection fixture replay seed`@limecloud/agent-runtime-projection` 新增 `replayAgentUiFixture`,标准 fixtures 可投影到 ProjectionState,并补 subagent graph node 识别。 | `packages/agent-runtime-projection/src/fixtureReplay.ts``packages/agent-runtime-projection/tests/fixtureReplay.test.mjs``npm --prefix packages/agent-runtime-projection run test` 通过。 |
| 2026-06-10 | 完成 React fixture smoke`@limecloud/agent-runtime-ui` 使用标准 fixture replay state 做 server render,覆盖 MessageParts、ProcessTimeline、ExecutionGraph、ActionRequired 与 subagent handoff graph。 | `packages/agent-runtime-ui/tests/ui.test.mjs``npm --prefix packages/agent-runtime-ui run test` 通过。 |
| 2026-06-10 | 建立多进程任务板,明确剩余 workstream、窄写集、current owner、禁止方向、验证命令和推荐下一刀。 | `internal/roadmap/agentworkbench/task-board.md`。 |
| 2026-06-10 | 完成 Runtime Client Conformance`@limecloud/agent-runtime-client/sessionGateway` 适配已有 App Server session gateway 为完整 `AgentRuntimeClient`,覆盖 lifecycle、readThread、exportEvidence、runtime event dispatch / nextEvent、transport error propagation 与 browser-safe 子路径。 | `packages/agent-runtime-client/src/sessionGateway.ts``packages/agent-runtime-client/tests/client.test.mjs``npm --prefix packages/agent-runtime-client run test` 通过。 |
| 2026-06-10 | 完成 Workstream 2App Server Facts Runner。`@limecloud/agent-runtime-projection` 新增纯 App Server facts adapter,把 `agentSession/event``agentSession/read``evidence/export` 投影为标准 execution events 和 `AgentUiProjectionState`。 | `packages/agent-runtime-projection/src/appServerFacts.ts``packages/agent-runtime-projection/tests/appServerFacts.test.mjs``npm --prefix packages/agent-runtime-projection run test` 通过。 |
| 2026-06-10 | 完成 Subagents 标准模型 seedcontracts 增加并校验完整 `AgentUiSubagentsModel`projection 输出 `state.subagents`React `SubagentsView` 只消费该模型。 | `packages/agent-ui-contracts/src/projection.ts``packages/agent-ui-contracts/src/validation.ts``packages/agent-runtime-projection/src/subagents.ts``packages/agent-runtime-ui/src/subagents.tsx`。 |
| 2026-06-10 | 补齐四包 README SDK 使用说明:current 包名、事件分类、lifecycle、transport、state/hydration、React surfaces、conformance、package metadata 与禁止方向。 | `packages/agent-ui-contracts/README.md``packages/agent-runtime-client/README.md``packages/agent-runtime-projection/README.md``packages/agent-runtime-ui/README.md`。 |
| 2026-06-10 | 完成 AgentUI 防回流治理 guard 验证:非标准树术语、本地 process owner、直连 Provider runtime 已进入 legacy surface catalog 机械守卫。 | `src/lib/governance/legacySurfaceCatalog.json``src/lib/governance/legacySurfaceCatalog.test.ts``npm test -- src/lib/governance/legacySurfaceCatalog.test.ts``npm run governance:legacy-report``npm run test:contracts` 通过。 |
| 2026-06-10 | 完成 Agent App seed adoption 验证:运行页默认宿主使用标准 `AgentRuntimeClient`Host Agent Run 面板使用共享 `AgentUiProjectionView`action callback 回到宿主。 | `src/features/agent-app/runtime/agentRuntimeClientApi.ts``src/features/agent-app/runtime/agentRuntimeAppServerClient.ts``src/features/agent-app/runtime/agentRunProjectionState.ts``src/features/agent-app/ui/AgentRunProjectionPanel.tsx`;相关定向测试通过。 |
| 2026-06-10 | 补强 Workbench SDK 文档站:contracts validation、types/events/fixtures、projection projector/selectors/replay、runtime-client events、runtime-ui Subagents 与 conformance 均对齐四包 current owner。 | `/Users/coso/Documents/dev/ai/limecloud/lime-agent-workbench/docs/sdk/typescript/**``npm run docs:build` 通过。 |
| 2026-06-10 | 补齐 Runtime UI Artifact / Evidence refs 标准 surfaceReact 包新增 `AgentUiRefList``ArtifactRefList``EvidenceRefList`,组合视图接入 artifact/evidence callbacks 和 labels。 | `packages/agent-runtime-ui/src/refs.tsx``packages/agent-runtime-ui/src/projectionView.tsx``packages/agent-runtime-ui/tests/ui.test.mjs``npm --prefix packages/agent-runtime-ui run test` 通过。 |
| 2026-06-10 | 按 AG-UI / AI SDK 的页面颗粒度补强 Lime 自有 SDK reference,但事实源仍是 Lime 四包:新增 event families、projection state、transport contract、provider boundary、callbacks、refs、conformance runner、AgentUI 最小面板页面。 | `/Users/coso/Documents/dev/ai/limecloud/lime-agent-workbench/docs/sdk/typescript/**``docs/quickstart/agentui-minimal-panel.md``docs/.vitepress/config.mts`。 |
| 2026-06-10 | 完成 Content Studio adoption blueprint:把 Host Provider Runtime PRD 转成 Workbench 接入指南,明确产品 owns / does not own、标准 surface 映射、compat 迁移表和 blocked 行为。 | `/Users/coso/Documents/dev/ai/limecloud/lime-agent-workbench/docs/tutorials/content-studio.md`。 |
| 2026-06-10 | 完成本轮验证:四包定向测试、Workbench 文档构建、Agent Runtime current fixture smoke、术语扫描和 diff whitespace 检查均通过。 | `npm --prefix packages/agent-ui-contracts run test``npm --prefix packages/agent-runtime-client run test``npm --prefix packages/agent-runtime-projection run test``npm --prefix packages/agent-runtime-ui run test``npm run smoke:agent-runtime-current-fixture``npm run docs:build`、非标准树形过程术语扫描无 current docs 命中。 |
| 2026-06-10 | 完成 Agent App 真实主路径 smoke:正式 `Agent Apps` 聚合入口启动 runtime iframeSDK 经 Host Bridge 调用 `startTask/getTask/submitHostResponse/cancelTask`App Server current `agentSession/*` 与 external backend 贯通,Host Agent Run 使用共享 `AgentUiProjectionView` 渲染标准 projection DOM。 | `scripts/agent-app/runtime-electron-sdk-fixture-smoke.mjs` 连续通过;summary 证据位于 `.lime/qc/gui-evidence/agent-app-runtime-electron-sdk-fixture/agent-app-runtime-electron-sdk-fixture-summary.json`,包含 `agentSession/start``agentSession/turn/start``agentSession/action/respond``agentSession/read``agentSession/turn/cancel``agent-app-sdk-artifact-1``evidence:.lime/artifacts/agent-app-sdk-artifact-1.json`。 |
| 2026-06-10 | 修正 App Server artifact evidence replay 进入标准 AgentUI projection 的映射:`evidence:recorded` 现在可从 runtime replay `refs` / `payload.artifactRef` 得到 `EvidenceRefList` DOMHost projection smoke 不再把已处理 action 误判为必须存在 pending `ActionRequiredList`。 | `src/features/agent-app/runtime/agentUiProjectionBridge.ts``src/features/agent-app/runtime/agentUiProjectionBridge.test.ts``src/features/agent-app/ui/AgentRunProjectionPanel.test.tsx``scripts/agent-app/runtime-electron-sdk-fixture-smoke.test.mjs``node --check``npx eslint --max-warnings 0 ...``npm test -- scripts/agent-app/runtime-electron-sdk-fixture-smoke.test.mjs src/features/agent-app/runtime/agentUiProjectionBridge.test.ts src/features/agent-app/ui/AgentRunProjectionPanel.test.tsx` 通过。 |
| 2026-06-11 | 完成 Content Studio 标准 UI surface 接入:`AI agents` 工作台和通用 `AgentSessionPanel` 统一通过 `AgentUiProjectionSurface` 消费共享 projection read model,产品页面不再直接拼共享 timeline / runtime facts primitives,并补边界审计阻止 primitives 回流。 | `/Users/coso/Documents/dev/ai/limecloud/content-studio/src/renderer/src/components/agent/AgentUiProjectionSurface.tsx``src/renderer/src/components/agent/AgentSessionPanel.tsx``src/renderer/src/components/agents/AgentsWorkbench.tsx``scripts/lime-agent-boundary-audit.mjs``npm run verify:lime-agent``npm run test:functional -- --test-name-pattern "Lime Agent 边界审计会阻断 runtime/key/UI 协议回流"`、相关 `git diff --check` 通过。 |
| 2026-06-11 | 完成 Content Studio session gateway 标准包接入:standalone/dev App Server turn 主链已通过 `@limecloud/agent-runtime-client/sessionGateway``createAgentRuntimeClientFromSessionGateway(...)` 包装为标准 `AgentRuntimeClient``nextEvent()` 保持标准 `agentSession/event` notification 形状,内部轮询通过标准 client 消费 notification 后再提取 runtime event。 | `/Users/coso/Documents/dev/ai/limecloud/content-studio/package.json``package-lock.json``src/main/services/appServerAgentRuntimeGateway.ts``scripts/lime-agent-boundary-audit.mjs``tests/functional/content-flow.test.mjs`、Content Studio LimeAgent 路线图工件;`npm run verify:lime-agent`、定向 functional、`npx tsc --noEmit --pretty false` 通过。 |
| 2026-06-11 | 补齐标准包发布前门槛:修复 `app-server-client` method catalog / schema fixture 漂移,补齐 `WorkspaceEnsureProject*` checked-in schema,两个标准包包级测试和 package dry-run 均通过。 | `packages/app-server-client/tests/client.test.mjs``lime-rs/crates/app-server-protocol/schema/json/**``lime-rs/crates/app-server-protocol/src/protocol/v0/tests/catalog.rs``npm --prefix packages/app-server-client run test``npm --prefix packages/agent-runtime-client run test``cargo test --manifest-path "lime-rs/Cargo.toml" -p app-server-protocol schema_fixtures_match_generated_output``npm pack --dry-run --json --ignore-scripts` 通过;该行是发布前 checkpoint,当前 registry 状态以后续 scoped 发布记录为准。 |
| 2026-06-11 | 固化标准包发布与 Content Studio 产品接入 runbook:明确 scoped 发布顺序、失败处理、禁止 `file:` / 绝对路径依赖、Content Studio runtime-client 接入步骤和 GUI smoke 完成判定。 | runbook 已更新为当前状态:`@limecloud/app-server-client@1.66.0``@limecloud/agent-runtime-client@0.1.1` 已发布并接入;早期 npm 认证失败和误发只作为历史记录。 |
| 2026-06-11 | 收掉 Agent App runtime bridge 巨型文件红线:把字段读取 / payload helper 抽到 `agentUiProjectionFieldReaders.ts`,把 `AgentUiProjectionEvent -> AgentRuntimeExecutionEvent` 适配抽到 `agentUiRuntimeEventAdapter.ts``agentUiProjectionBridge.ts``1253` 行降到 `895` 行。 | `src/features/agent-app/runtime/agentUiProjectionBridge.ts``src/features/agent-app/runtime/agentUiProjectionFieldReaders.ts``src/features/agent-app/runtime/agentUiRuntimeEventAdapter.ts``npm test -- src/features/agent-app/runtime/agentUiProjectionBridge.test.ts src/features/agent-app/runtime/agentUiProjectionViewModel.test.ts src/features/agent-app/ui/AgentRunProjectionPanel.test.tsx``npx eslint ... --max-warnings 0``git diff --check` 通过。 |
| 2026-06-11 | 收缩 Agent App Host run 本地过程面:Host action 测试迁到 current `respondAction -> agentSession/action/respond``AgentRunHostDrawer` 拆成宿主壳、projection input enrichment 和本地 fallback,主抽屉降到 `382` 行;标准 projection 有内容时不再混入本地 process view。 | `src/features/agent-app/ui/AgentAppRuntimePage.agentRun.test.tsx``src/features/agent-app/ui/AgentRunHostDrawer.tsx``src/features/agent-app/ui/AgentRunHostDrawerFallback.tsx``src/features/agent-app/ui/AgentRunHostDrawerProjectionInput.ts``npx eslint ... --max-warnings 0``npm test -- src/features/agent-app/ui/AgentAppRuntimePage.agentRun.test.tsx src/features/agent-app/ui/AgentRunProjectionPanel.test.tsx src/features/agent-app/runtime/agentUiProjectionViewModel.test.ts src/features/agent-app/runtime/agentUiProjectionBridge.test.ts src/features/agent-app/runtime/agentRunProjectionState.test.ts``git diff --check` 通过。 |
| 2026-06-11 | 完成 Agent App projection builders 二次拆分:`agentUiProjectionBridge.ts` 只保留 task event normalization、sequence 和 runtime event facadeprojection builders 进入 `agentUiProjectionBuilders.ts`owner / scope / status / control mapping 进入 `agentUiProjectionMapping.ts`,所有拆分文件均低于 `800` 行预警线。 | `src/features/agent-app/runtime/agentUiProjectionBridge.ts``src/features/agent-app/runtime/agentUiProjectionBuilders.ts``src/features/agent-app/runtime/agentUiProjectionMapping.ts``npx eslint ... --max-warnings 0``npm test -- src/features/agent-app/ui/AgentAppRuntimePage.agentRun.test.tsx src/features/agent-app/ui/AgentRunProjectionPanel.test.tsx src/features/agent-app/runtime/agentUiProjectionViewModel.test.ts src/features/agent-app/runtime/agentUiProjectionBridge.test.ts src/features/agent-app/runtime/agentRunProjectionState.test.ts``git diff --check` 通过。 |
| 2026-06-11 | 补 Agent App projection 边界结构守卫:新增测试防止 `agentUiProjectionBridge.ts` 重新承接 builders / mapping 职责,并机械检查 bridge、builders、mapping、field readers、runtime adapter 均低于 `800` 行预警线。 | `src/features/agent-app/runtime/agentUiProjectionBoundary.test.ts``npx eslint src/features/agent-app/runtime/agentUiProjectionBoundary.test.ts ... --max-warnings 0``npm test -- src/features/agent-app/runtime/agentUiProjectionBoundary.test.ts src/features/agent-app/runtime/agentUiProjectionBridge.test.ts``git diff --check` 通过。 |
| 2026-06-11 | 补 Agent App HostDrawer 边界结构守卫:新增测试防止主抽屉直接 import `InlineToolProcessStep` / `MarkdownRenderer` / `ThinkingBlock` 等本地 process 组件,并要求本地 fallback 只能作为退场 residual 存在。 | `src/features/agent-app/ui/AgentRunHostDrawerBoundary.test.ts``npx eslint src/features/agent-app/ui/AgentRunHostDrawerBoundary.test.ts ... --max-warnings 0``npm test -- src/features/agent-app/ui/AgentRunHostDrawerBoundary.test.ts src/features/agent-app/ui/AgentAppRuntimePage.agentRun.test.tsx src/features/agent-app/ui/AgentRunProjectionPanel.test.tsx``git diff --check` 通过。 |
| 2026-06-11 | 完成 Agent App HostDrawer 主路径标准化收口:`AgentRunHostDrawer` 始终渲染标准 `AgentRunProjectionPanel`,不再 import 或调用本地 process fallback,空态也交给标准 projection`AgentRunHostDrawerFallback.tsx` 仅作为待删除的 `deprecated` residual 保留。 | `src/features/agent-app/ui/AgentRunHostDrawer.tsx``src/features/agent-app/ui/AgentRunHostDrawerBoundary.test.ts``npx eslint src/features/agent-app/ui/AgentRunHostDrawer.tsx src/features/agent-app/ui/AgentRunHostDrawerBoundary.test.ts src/features/agent-app/ui/AgentRunHostDrawerFallback.tsx src/features/agent-app/ui/AgentRunHostDrawerProjectionInput.ts --max-warnings 0``npm test -- src/features/agent-app/ui/AgentRunHostDrawerBoundary.test.ts src/features/agent-app/ui/AgentAppRuntimePage.agentRun.test.tsx src/features/agent-app/ui/AgentRunProjectionPanel.test.tsx``git diff --check` 通过。 |
| 2026-06-11 | 完成 Agent App HostDrawer fallback 防回流治理升级:`legacySurfaceCatalog` 新增 `agent-app-host-drawer-local-process-fallback`,将主抽屉重新调用本地 fallback 归类为 `dead`contract guard 更新为 current `sharedProjectionInput -> buildAgentRunStandardProjectionStateFromState(sharedProjectionInput)` 形状。 | `src/lib/governance/legacySurfaceCatalog.json``src/lib/governance/legacySurfaceCatalog.test.ts``scripts/check-app-server-client-contract.mjs``npm test -- src/lib/governance/legacySurfaceCatalog.test.ts``npm run governance:legacy-report``npm run test:contracts``git diff --check` 通过。 |
| 2026-06-11 | 历史 checkpoint:首次复核标准包发布门槛时 npm 认证未就绪,因此停在未发布状态。 | 该行只解释早期阻塞来源;当前 scoped registry 发布状态以后续 `@limecloud/app-server-client@1.66.0``@limecloud/agent-runtime-client@0.1.1` 发布记录为准。 |
| 2026-06-11 | 修正标准包发布口径:无 scope `app-server-client@1.66.0` 已误发到个人包列表,`@limecloud/agent-runtime-client@0.1.0` 已误依赖旧名;最终标准链路改为 `@limecloud/app-server-client@1.66.0``@limecloud/agent-runtime-client@0.1.1`,开发态通过 scoped alias 指向本地源码。 | `packages/app-server-client/package.json``packages/agent-runtime-client/package.json``packages/agent-runtime-client/src/**``electron/**``vite.config.ts``scripts/electron/build-host.mjs``packages/agent-app-runtime/build.mjs``package-lock.json``internal/roadmap/agentworkbench/standard-package-release-runbook.md`。 |
| 2026-06-11 | 完成 scoped 标准包 registry 发布与 Content Studio 接入复核:`@limecloud/app-server-client@1.66.0``@limecloud/agent-runtime-client@0.1.1` 均可从 npm registry 查询,Content Studio 固定依赖 `@limecloud/agent-runtime-client: 0.1.1`lockfile 解析到 `@limecloud` organization tarball,并禁止无 scope `app-server-client` 回流。 | `npm view "@limecloud/app-server-client@1.66.0"``npm view "@limecloud/agent-runtime-client@0.1.1"``/Users/coso/Documents/dev/ai/limecloud/content-studio/package.json``package-lock.json``scripts/lime-agent-boundary-audit.mjs`。 |
## 完成判定
本次迭代完成需要同时满足:
1. `lime-agent-workbench` 文档站覆盖 P0.1-P0.3。
2. 导航可访问新增页面。
3. `npm run docs:build` 通过。
4. 本路线图进度日志记录已完成项和剩余缺口。
5. 未覆盖其他进程的 Lime 主仓 dirty diff。
## 当前完成度
路线图工件完成度:`100%`
Workbench / Lime AgentUI 标准整体完成度:`98.5%`。当前已完成 P0 文档标准页、导航接入、构建验证、contracts seed、projection fixture replay、App Server facts runner、Subagents 标准模型、Artifact/Evidence refs React surface、React fixture smoke、runtime-client conformance、四包 README SDK 化、治理 guard 验证、Agent App 真实主路径 smoke、Content Studio adoption blueprint、Content Studio 标准 UI surface 接入、Content Studio session gateway 形状收敛、scoped 标准包 registry 发布、Content Studio 固定安装并接入 `@limecloud/agent-runtime-client/sessionGateway`、标准包发布前测试 / pack dry-run 门槛、标准包发布 / Content Studio 接入 runbook、Agent App runtime bridge 巨型文件拆分、Agent App Host run 本地过程面收缩、Agent App projection builders / mapping 二次拆分、Agent App projection 边界结构守卫、Agent App HostDrawer 边界结构守卫、Agent App HostDrawer 始终渲染标准 projection 的主路径收口,以及 HostDrawer fallback 防回流治理 catalog / contract guard;剩余缺口是 Content Studio GUI smoke / 产品回归、Agent App 本地 process fallback 文件物理删除、正式版本发布,以及需要真实 Provider 授权时的 live evidence。完成度暂不升到 `100%`,因为真实页面 smoke 仍未闭环。
后续拆分风险:`src/features/agent-app/runtime/agentUiProjectionBridge.ts` 已降到 `82` 行,`agentUiProjectionBuilders.ts``482` 行,`agentUiProjectionMapping.ts``373` 行,`src/features/agent-app/ui/AgentRunHostDrawer.tsx` 已降到 `382` 行,当前不再触碰 `800` 行预警和 `1000` 行硬红线;新增 Agent App 协议映射必须进入 builders / mapping / field readers 对应边界,不得塞回 bridge。`AgentRunHostDrawerFallback.tsx` 已退出主路径,仅作为待删除的 `deprecated` residual,不能继续承接标准过程展示或 projection 空态。
@@ -0,0 +1,120 @@
# Agent Workbench 并行工作流
> 状态:active
> 目的:允许多个进程同时推进 `lime-agent-workbench`,减少互相覆盖。
## 并行前置规则
每个进程开始前必须先执行:
```bash
git status --short
```
然后声明本轮只认领一个窄写集。不要多个进程同时编辑同一文件;导航文件和版本文件由最后一个集成进程统一修改。
当前实现阶段的任务认领以 [task-board.md](task-board.md) 为准;本文件保留文档站阶段的并行拆分和通用协作规则。
## Workstream ASubagents 标准
| 项 | 内容 |
| --- | --- |
| 主目标 | 补齐 `/subagents` 顶层标准页。 |
| 推荐写集 | `docs/subagents.md`、必要时 `docs/concepts/agents.md`。 |
| 暂不触碰 | `docs/.vitepress/config.mts`,除非当前进程负责集成导航。 |
| 依赖 | Runtime 事件、TaskSnapshot、ExecutionGraph、Subagents 概念。 |
| 验收 | 页面明确 runtime facts、UI surfaces、control plane、降级状态和禁止方向。 |
建议章节:
1. Subagents 在 Lime 中是什么。
2. Runtime 事实与 scope ids。
3. ExecutionGraph / Subagents 投影。
4. Handoff / review / worker notification。
5. 与 Agent App / Content Studio 的采用边界。
6. Conformance fixture。
7. current / compat / deprecated / dead。
## Workstream BSDK 包边界
| 项 | 内容 |
| --- | --- |
| 主目标 | 把 TypeScript SDK 文档从包名规划补成实现边界。 |
| 推荐写集 | `docs/sdk/typescript/**`。 |
| 暂不触碰 | `packages/**` 生产代码,除非另行认领实现任务。 |
| 依赖 | AI SDK `UIMessage.parts` 参考、AG-UI event stream 参考、Lime current packages。 |
| 验收 | 四包职责、依赖方向、API 草案、fixture replay、版本策略完整。 |
建议新增或补强页面:
- `docs/sdk/typescript/react-surfaces.md`
- `docs/sdk/typescript/conformance.md`
- `docs/sdk/typescript/package-boundaries.md`
## Workstream CRuntime 配合
| 项 | 内容 |
| --- | --- |
| 主目标 | 把 App Server / RuntimeCore / ExecutionBackend 与 AgentUI 的配合写成实现合同。 |
| 推荐写集 | `docs/contracts/app-server-host.md``docs/contracts/runtime-read-model.md``docs/quickstart/runtime-provider.md`。 |
| 暂不触碰 | Lime 主仓 `src/lib/dev-bridge/**``lime-rs/**`。 |
| 依赖 | `internal/roadmap/agentruntime/README.md``internal/roadmap/agentruntime/agentruntime-standard-adoption-gap.md`。 |
| 验收 | Runtime provider 不依赖 ReactUI 不拥有 factsread model / event stream repair / action response 有明确 owner。 |
## Workstream DContent Studio / Product App 接入
| 项 | 内容 |
| --- | --- |
| 主目标 | 把 Workbench 标准落到 Content Studio 和未来 Agent Apps 的接入路径。 |
| 推荐写集 | `docs/tutorials/content-studio.md``docs/profiles/content-studio.md``docs/quickstart/product-app.md`。 |
| 暂不触碰 | `content-studio` 生产代码。 |
| 依赖 | Host Provider Runtime PRD、Agent App runtime roadmap。 |
| 验收 | 产品应用只提交业务 context 和 callbacks,不传 Provider key、不读 App Server DB、不自建 process component。 |
## Workstream E:导航 / 构建 / 发布集成
| 项 | 内容 |
| --- | --- |
| 主目标 | 统一接入 nav/sidebar、构建验证和发布准备。 |
| 推荐写集 | `docs/.vitepress/config.mts``README.md``docs/development/updates.md``package.json``package-lock.json`。 |
| 前置 | A-D 至少一个内容页已完成。 |
| 验收 | `npm run docs:build` 通过;版本和更新记录一致。 |
注意:修改版本号、commit、tag、push 属于高风险操作,必须由用户明确要求后执行。
## 推荐并行顺序
```text
A Subagents 标准
B SDK 包边界
C Runtime 配合
D Product App 接入
-> E 导航 / 构建 / 发布集成
```
如果只开两个进程:
| 进程 | 认领 |
| --- | --- |
| 进程 1 | A + C |
| 进程 2 | B + D |
| 集成进程 | E |
## 冲突处理
| 情况 | 处理 |
| --- | --- |
| 发现目标文件已有未理解改动 | 停止写入该文件,切只读审阅。 |
| 两个进程都需要改导航 | 先不接导航,完成内容页后交给 E。 |
| 发现文档标准和 Lime current 实现冲突 | 先写 conflict note 到对应页面,不直接改生产代码。 |
| 发现需要生产实现才能闭环 | 回写 `iteration-plan.md` 的 P1/P2,不在文档进程里夹写代码。 |
## 每个进程收尾格式
每个进程完成后,在 `iteration-plan.md` 追加一行进度日志:
```markdown
| 2026-06-10 | 完成 Workstream ASubagents 标准页。 | `docs/subagents.md``npm run docs:build`。 |
```
如果没有跑构建,要明确写原因和下一步。
@@ -0,0 +1,162 @@
# Agent Workbench 标准包发布与产品接入 Runbook
> 状态:published-and-adopted; gui-smoke-pending
> 更新时间:2026-06-11
> 适用范围:`@limecloud/app-server-client@1.66.0``@limecloud/agent-runtime-client@0.1.1`、Content Studio 标准 runtime-client 接入。
## 目标
把已通过本地 conformance 的 Agent Runtime 标准包发布到 npm registry,然后让 Content Studio 通过正式 npm 依赖消费 `@limecloud/agent-runtime-client/sessionGateway`,完成:
```text
Content Studio App Server session gateway
-> @limecloud/agent-runtime-client/sessionGateway
-> @limecloud/agent-runtime-projection
-> @limecloud/agent-runtime-ui
-> AgentUiProjectionSurface
```
scoped 发布和产品依赖接入已完成;剩余 `98.5% -> 100%` 的主线缺口是 Content Studio GUI smoke / 产品回归。不得使用 `file:`、本机绝对路径、tarball 临时路径或复制源码的方式伪装完成。
## 当前状态
| 项 | 状态 | 证据 |
| --- | --- | --- |
| `@limecloud/app-server-client@1.66.0` | published | 已发布到 `@limecloud` organization;本地开发仍通过 scoped alias 指向 `packages/app-server-client/src/index.ts`。 |
| `@limecloud/agent-runtime-client@0.1.1` | published | 已发布到 `@limecloud` organizationruntime-client 依赖 `@limecloud/app-server-client@1.66.0`。 |
| `app-server-client@1.66.0` | compat-misrelease | 已误发布到 `wutongci` 用户包列表;不作为 Lime 标准链路完成证据,不继续扩展。 |
| `@limecloud/agent-runtime-client@0.1.0` | compat-misrelease | 已发布但依赖无 scope `app-server-client`;不作为 Content Studio 接入版本。 |
| npm 登录状态 | ready | `npm whoami -> wutongci`,账号对 `@limecloud` organization 包有 read-write 权限。 |
| Content Studio runtime-client 接入 | adopted | Content Studio 固定安装 registry 版 `@limecloud/agent-runtime-client@0.1.1`lockfile 解析到间接 `@limecloud/app-server-client@1.66.0`,主路径通过 `@limecloud/agent-runtime-client/sessionGateway` 进入标准 `AgentRuntimeClient`。 |
## 风险分类
| 分类 | 路径 / 行为 | 规则 |
| --- | --- | --- |
| current | `@limecloud/app-server-client` | 当前 `@limecloud/agent-runtime-client` 的 App Server JSON-RPC 依赖包;开发态通过 scoped alias 指向本地 `packages/app-server-client/src/index.ts`。 |
| current | `@limecloud/agent-runtime-client/sessionGateway` | 标准 runtime client facade,负责 lifecycle、event、read、action response、evidence export。 |
| current | Content Studio `ContentStudioAgentRuntimeSessionGateway` | 产品侧 session gateway 适配层,已压薄为标准包 adapter,只保留 `agentSession/start``artifact/read` 和 sidecar transport。 |
| compat | Content Studio 本地 gateway 形状 | 只允许作为标准 runtime-client adapter 的产品侧补充,不允许继续长第二套 runtime client。 |
| dead | `file:` / 绝对路径依赖 / 复制 dist | 不允许作为产品接入完成证据。 |
## Registry 复核
当前已完成发布,复核时在 Lime 主仓运行:
```bash
npm view "@limecloud/app-server-client@1.66.0" version
npm view "@limecloud/agent-runtime-client@0.1.1" version
npm view "@limecloud/agent-runtime-client@0.1.1" dependencies
```
当前通过标准:
- `npm view "@limecloud/app-server-client@1.66.0"` 返回 `1.66.0`
- `npm view "@limecloud/agent-runtime-client@0.1.1"` 返回 `0.1.1`
- `@limecloud/agent-runtime-client@0.1.1` 依赖 `@limecloud/app-server-client: 1.66.0`
- 无 scope `app-server-client@1.66.0``@limecloud/agent-runtime-client@0.1.0` 只保留为 `compat-misrelease` 记录,不作为任何产品接入证据。
## 已通过的发布门槛
发布前已在 Lime 主仓通过:
```bash
npm --prefix packages/app-server-client run test
npm --prefix packages/agent-runtime-client run test
cargo test --manifest-path "lime-rs/Cargo.toml" -p app-server-protocol schema_fixtures_match_generated_output
cargo test --manifest-path "lime-rs/Cargo.toml" -p app-server-protocol app_server_method_catalog_keeps_request_and_notification_methods_together
cd packages/app-server-client && npm pack --dry-run --json --ignore-scripts
cd ../agent-runtime-client && npm pack --dry-run --json --ignore-scripts
```
通过标准:
- 两个 dry-run 包只包含 `README.md``dist/**``package.json`
- 不从 workspace root 运行目标包 pack,避免误把根包当成发布包。
## 发布动作记录
已按以下顺序发布 scoped 标准包:
```bash
cd packages/app-server-client
npm publish --access public
cd ../agent-runtime-client
npm publish --access public
```
发布后验证:
```bash
npm view "@limecloud/app-server-client@1.66.0" version
npm view "@limecloud/agent-runtime-client@0.1.1" version
```
后续如果需要新版本发布,仍必须重新走用户明确确认;commit、tag、push 也不自动执行。
## Content Studio 接入状态
路径:`/Users/coso/Documents/dev/ai/limecloud/content-studio`
已固定依赖:
```bash
"@limecloud/agent-runtime-client": "0.1.1"
```
已确认:
- lockfile 解析到 `@limecloud/agent-runtime-client@0.1.1`
- 间接依赖解析到 `@limecloud/app-server-client@1.66.0`
- lockfile 不再解析到无 scope `app-server-client`
- `package.json` 不出现 `file:`、绝对路径或 tarball 临时路径。
代码接入结果:
1. Content Studio 主进程侧从 `@limecloud/agent-runtime-client/sessionGateway` 导入 `createAgentRuntimeClientFromSessionGateway`
2. 现有 `ContentStudioAgentRuntimeSessionGateway` 被包装成标准 `AgentRuntimeClient`
3. `startTurn/readThread/cancelTurn/respondAction/exportEvidence/nextEvent` 经过标准 client facade。
4. `ContentStudioAgentRuntimeSessionGateway.nextEvent()` 保留 `agentSession/event` notification 形状。
5. 边界审计已要求实际导入 `@limecloud/agent-runtime-client/sessionGateway`,并检查 scoped registry lockfile。
禁止:
- 不把 `nextEvent()` 改回裸 `RuntimeEvent`
- 不在 Content Studio 复制 `@limecloud/agent-runtime-client` 的实现。
- 不让 renderer 直连 Provider key 或 App Server DB。
- 不恢复本地过程组件作为标准 AgentUI owner。
## Content Studio 验证
已运行:
```bash
npm run verify:lime-agent
npm run test:functional -- --test-name-pattern "Lime Agent 边界审计会阻断 runtime/key/UI 协议回流"
npx tsc --noEmit --pretty false
```
仍需补 GUI smoke / 产品回归,证明真实页面存在并可运行:
```text
.agent-ui-projection
.agent-ui-main
.agent-ui-sidecar
```
GUI smoke 未完成前,不得把整体目标标为 `100%`
## 完成判定
只有同时满足以下条件,才能把整体目标从 `98.5%` 更新为 `100%`
1. `@limecloud/app-server-client@1.66.0``@limecloud/agent-runtime-client@0.1.1` 都可从 npm registry 查询。
2. Content Studio 通过正式 npm 依赖安装 `@limecloud/agent-runtime-client@0.1.1`
3. Content Studio 主路径实际消费 `@limecloud/agent-runtime-client/sessionGateway`
4. `runtime-client -> projection -> runtime-ui -> AgentUiProjectionSurface` 链路有产品验证。
5. Content Studio GUI smoke / 产品回归通过。
## 当前剩余缺口
截至 `2026-06-11`scoped registry 发布和 Content Studio runtime-client 接入已完成。下一刀是 Content Studio GUI smoke / 产品回归,证明 `.agent-ui-projection` / `.agent-ui-main` / `.agent-ui-sidecar` 在真实页面可运行。已误发的无 scope `app-server-client@1.66.0` 与依赖旧名的 `@limecloud/agent-runtime-client@0.1.0` 只作为 compat-misrelease 记录,不进入 Content Studio 接入。
@@ -0,0 +1,374 @@
# Agent Workbench 当前迭代任务板
> 状态:active
> 更新时间:2026-06-11
> 目的:把本次 Agent Workbench 迭代拆成可并行认领的窄任务,后续多开进程时以本文件为任务入口。
## 主目标
把 Agent Workbench 从文档站推进成 Lime AgentUI / AgentRuntime 标准指引和实现种子。所有新增实现必须继续向以下 current 主链收敛:
```text
App Server / RuntimeCore / ExecutionBackend
-> RuntimeEvent / ThreadReadModel / TaskSnapshot / EvidencePack
-> @limecloud/agent-ui-contracts
-> @limecloud/agent-runtime-projection
-> @limecloud/agent-runtime-ui
-> Product Apps / Content Studio / Agent Apps
```
外部 AG-UI、AI SDK、OpenAI Agents JS、LangGraph 等只作为参考;不能成为 Lime runtime owner,也不能让产品应用绕开 App Server / RuntimeCore 直接拥有 Provider key 或 runtime facts。
## 当前完成度
| 维度 | 状态 | 证据 |
| --- | --- | --- |
| Workbench P0 文档站 | done | Subagents、SDK、runtime host、read model、provider 文档已补齐,`npm run docs:build` 已通过。 |
| Contracts seed | done | `@limecloud/agent-ui-contracts` 已有 fixtures、validation、完整 `AgentUiSubagentsModel`、subagent / handoff / review scope ids。 |
| Projection replay seed | done | `@limecloud/agent-runtime-projection` 已能 replay contracts fixtures,并输出 `state.subagents`。 |
| React fixture smoke | done | `@limecloud/agent-runtime-ui` 已用 fixture replay state 做 server render smokeSubagents 只消费 `state.subagents`。 |
| Runtime client conformance | done | `@limecloud/agent-runtime-client/sessionGateway` 已补完整 runtime client conformance,覆盖 lifecycle、read、evidence export、event dispatch / nextEvent、transport error 和 browser-safe 子路径。 |
| App Server facts runner | done | `@limecloud/agent-runtime-projection` 已有纯 App Server facts adapter,可 replay `agentSession/event``agentSession/read``evidence/export` 到标准 projection。 |
| SDK package README | done | 四包 README 已补 current 包名、生命周期、transport、state/hydration、React surface、conformance、package metadata。 |
| Governance guard | done | 已通过 legacy surface catalog、catalog test、legacy report 和 contract guard 防止非标准树术语、本地 process owner、HostDrawer fallback 回流、provider direct API、第二套 runtime facts 回流。 |
| Artifact / Evidence refs React surface | done | `@limecloud/agent-runtime-ui` 已新增 `ArtifactRefList``EvidenceRefList``AgentUiRefList`,组合视图输出 refs DOM contract,包级测试通过。 |
| SDK reference completeness | done | Workbench 新增 event families、projection state、transport、provider boundary、callbacks、refs、conformance runner、minimal panel 页面,导航已接入。 |
| Standard package publish gate | done | `@limecloud/app-server-client@1.66.0``@limecloud/agent-runtime-client@0.1.1` 已发布到 `@limecloud` organization;无 scope `app-server-client@1.66.0` 和依赖旧名的 `@limecloud/agent-runtime-client@0.1.0` 只作为 compat-misrelease 记录,不作为标准完成证据。 |
| Product app seed adoption | partial | Agent App 真实 Electron 主路径已证明复用标准 `AgentRuntimeClient`、共享 `AgentUiProjectionView` 与 Artifact/Evidence refsHost action 决策已走 current `respondAction -> agentSession/action/respond`Host drawer 已成为始终渲染标准 projection 的宿主壳;Content Studio UI 主路径已统一到标准 `AgentUiProjectionSurface`,并已通过 registry 版 `@limecloud/agent-runtime-client@0.1.1/sessionGateway` 包装 App Server turn 主链,GUI smoke 仍待补。 |
整体目标完成度:`98.5%`。口径见 [acceptance.md](acceptance.md)。
## 并行认领规则
1. 开始前先执行 `git status --short`,只读盘点目标写集。
2. 每个进程只认领一个 workstream,不跨写集夹写。
3. 发现目标文件已有未理解改动,停止写入该文件,切只读审阅。
4. `iteration-plan.md` 只追加进度日志,不重写其他进程的历史记录。
5. 修改命令边界、runtime client、App Server host、DevBridge 或 mock 时,必须同时回到 `current / compat / deprecated / dead` 分类。
6. 不主动执行 `git commit``git push`、tag、版本发布;这些只在用户明确要求后做。
## Workstream 0:集成协调
| 项 | 内容 |
| --- | --- |
| 状态 | active |
| 推荐负责人 | 单进程 |
| 写集 | `internal/roadmap/agentworkbench/**` |
| 禁止夹写 | `packages/**``src/**``lime-rs/**` |
| 目标 | 维护本任务板、合并各进程进度、更新完成度口径。 |
验收:
- 每个完成的 workstream 都在 `iteration-plan.md` 增加进度日志。
- `README.md``parallel-workstreams.md``acceptance.md` 与本文件不冲突。
- 新增任务均有写集、验证命令和 current owner。
## Workstream 1Runtime Client Conformance
| 项 | 内容 |
| --- | --- |
| 状态 | done |
| 写集 | `packages/agent-runtime-client/**` |
| 前置 | 先盘点该目录 dirty diff;若发现其他进程正在编辑,停止写入。 |
| current owner | `@limecloud/agent-runtime-client` 只拥有 App Server JSON-RPC、Host bridge、events、read APIs、action response、evidence export。 |
| 禁止方向 | 生成 projection state、包含 React、持有 Provider key、直读 App Server DB、内置 mock fallback 作为生产路径。 |
任务:
- 盘点现有 `README.md``package.json``src/index.ts``tests/client.test.mjs``src/sessionGateway.ts` dirty diff。
- 补齐 startTurn、subscribeEvents、getThreadReadModel、respondAction、cancelTurn、exportEvidence 的最小 conformance。
- 补 transport error、stream interruption、action response、evidence export 的错误分类测试。
- 文档明确 runtime client 与 projection / react 的依赖边界。
验证:
```bash
npm --prefix packages/agent-runtime-client run test
```
若改动触及 App Server JSON-RPC 方法名、bridge 或 mock 边界,还要运行:
```bash
npm run test:contracts
```
完成证据:
- runtime client 不导出 UI projection state。
- tests 覆盖正常路径和错误路径。
- `iteration-plan.md` 追加日志。
当前证据:
- `packages/agent-runtime-client/src/sessionGateway.ts` 返回完整 `AgentRuntimeClient`,只适配 App Server session gateway,不生成 projection state。
- `packages/agent-runtime-client/tests/client.test.mjs` 覆盖 lifecycle、`exportEvidence``dispatchEvent``nextEvent``drainEvents`、缺 surface fail closed、transport error propagation 和 browser-safe bundle guard。
- `npm --prefix packages/agent-runtime-client run test` 通过。
## Workstream 2App Server Facts Runner
| 项 | 内容 |
| --- | --- |
| 状态 | done |
| 写集 | 优先 `packages/agent-runtime-projection/**`;如必须接 App Server client,另行认领 `packages/app-server-client/**`。 |
| current owner | App Server / RuntimeCore 输出 factsprojection 只消费 facts 并生成 UI state。 |
| 禁止方向 | 在 projection 内发起 Provider 请求、直读数据库、根据 assistant prose 推断工具或 evidence 状态。 |
任务:
- 定义从 App Server read model / facts export 到 `RuntimeEvent[]` 或 fixture replay 输入的 adapter。
- 支持 snapshot reconciliation、stream repair、大输出 refs、subagent scope ids。
- 添加 replay 幂等测试,覆盖至少 text、tool、hitl、artifact/evidence、subagent handoff。
- 若需要新增 fixture,只放在 contracts 包或 projection test fixture,不在产品应用里私有定义。
验证:
```bash
npm --prefix packages/agent-runtime-projection run test
```
必要时加:
```bash
npm --prefix packages/agent-ui-contracts run test
```
完成证据:
- App Server facts 可进入标准 projection replay。
- projection 不拥有 runtime truth。
- `iteration-plan.md` 追加日志。
当前证据:
- `packages/agent-runtime-projection/src/appServerFacts.ts` 新增 headless adapter,消费 `agentSession/event``agentSession/read``evidence/export` facts,输出标准 `AgentRuntimeExecutionEvent[]``AgentUiProjectionState`
- adapter 不创建 App Server client、不订阅 JSON-RPC、不直读数据库;`waitingAction` 映射为 `action.required` + waiting runtime state。
- `packages/agent-runtime-projection/tests/appServerFacts.test.mjs` 覆盖 text、tool、HITL、artifact/evidence、read model hydration、evidence export 与幂等去重。
- `npm --prefix packages/agent-runtime-projection run test` 通过。
## Workstream 3Governance Guard
| 项 | 内容 |
| --- | --- |
| 状态 | done |
| 写集 | 优先 `src/lib/governance/**`、相关 contract / governance tests;如新增脚本,必须放 `scripts/<domain>/` 或所属 package。 |
| current owner | 仓库治理 catalog / contract tests 是防回流事实源。 |
| 禁止方向 | 只写文档提醒、不加机械守卫;把 mock fallback 当生产可交付路径。 |
任务:
- 增加或扩展扫描规则,防止非标准树形过程术语作为标准术语回流。
- 防止产品应用新增本地 process component 作为 AgentUI 标准 owner。
- 防止生产路径直连 Provider API 绕开 App Server / RuntimeCore。
- 防止第二套 Agent App runtime facts 与标准 SDK 平级扩张。
- 对允许存在的历史引用标注 `test-only``retired``historical reference`
验证:
```bash
npm run governance:legacy-report
npm run test:contracts
```
若新增或移动脚本:
```bash
npm run governance:scripts
```
完成证据:
- 旧路被 catalog 或 contract test 机械封住。
- 保留的 compat / deprecated 均有退出条件。
- `iteration-plan.md` 追加日志。
当前证据:
- `src/lib/governance/legacySurfaceCatalog.json` 已登记 `agent-ui-nonstandard-tree-terminology``agent-ui-local-process-owner-terminology``agent-app-host-drawer-local-process-fallback``agent-ui-direct-provider-runtime-surface`
- `src/lib/governance/legacySurfaceCatalog.test.ts` 断言 AgentUI seed 包不得恢复非标准树术语。
- `scripts/check-app-server-client-contract.mjs` 已对齐当前 `sharedProjectionInput -> buildAgentRunStandardProjectionStateFromState(sharedProjectionInput)` 接线,防止 contract guard 把实现拉回旧参数形状。
- `npm test -- src/lib/governance/legacySurfaceCatalog.test.ts``npm run governance:legacy-report``npm run test:contracts` 通过。
## Workstream 4Product App Adoption Blueprint
| 项 | 内容 |
| --- | --- |
| 状态 | partial |
| 写集 | 优先 Workbench 文档:`/Users/coso/Documents/dev/ai/limecloud/lime-agent-workbench/docs/tutorials/**``docs/profiles/**`Content Studio 产品接入另行认领 `/Users/coso/Documents/dev/ai/limecloud/content-studio/src/renderer/src/components/agent/**``/Users/coso/Documents/dev/ai/limecloud/content-studio/src/renderer/src/components/agents/**``/Users/coso/Documents/dev/ai/limecloud/content-studio/scripts/lime-agent-boundary-audit.mjs`。 |
| current owner | 产品应用只提供业务 context、callbacks、workspace surfacesruntime facts 来自 runtime client + projection。 |
| 禁止方向 | Content Studio / Agent Apps 自建第二套过程组件、直接拼 Provider params、复制 projection reducer。 |
任务:
- 用 Content Studio Host Provider Runtime PRD 作为第一条 adoption blueprint。
- 写清 Product App 需要提供的 business context、artifact workspace、action callbacks、i18n keys。
- 写清 Product App 不应该拥有的 runtime client、tool state machine、subagent lineage truth。
- 给出迁移表:local process panel -> shared ProcessTimeline / ExecutionGraph / ActionRequired。
验证:
```bash
cd /Users/coso/Documents/dev/ai/limecloud/lime-agent-workbench
npm run docs:build
```
完成证据:
- Workbench 文档能指导 Content Studio 接入共享 AgentUI。
- 未触碰生产代码或已另行登记生产写集。
- `iteration-plan.md` 追加日志。
当前证据:
- `src/features/agent-app/runtime/agentRuntimeClientApi.ts` 把 Agent App `startTask / getTask / cancelTask / submitHostResponse` 适配到标准 `AgentRuntimeClient.startTurn / readThread / cancelTurn / respondAction`
- `src/features/agent-app/runtime/agentRuntimeAppServerClient.ts` 使用 `@limecloud/agent-runtime-client/sessionGateway` 包装 App Server `agentSession/*` current 方法。
- `src/features/agent-app/ui/AgentRunProjectionPanel.tsx` 渲染共享 `AgentUiProjectionView`,并把标准 action callback 回传宿主。
- `src/features/agent-app/ui/AgentAppRuntimePage.agentRun.test.tsx` 已把 Host action 断言迁到 current `appServerClientMocks.respondAction`,拒绝动作精确点击 action-required 列表内按钮,避免旧 Host response mock 被误当成 current 完成证据。
- `scripts/agent-app/runtime-electron-sdk-fixture-smoke.mjs` 已通过真实 Electron fixture`Agent Apps` 聚合入口 -> runtime iframe SDK -> App Server current `agentSession/*` -> Host Agent Run 标准 projection DOM。
- `src/features/agent-app/runtime/agentUiProjectionBridge.ts` 已把 App Server artifact replay 的 `evidence:recorded` refs 投影到标准 `EvidenceRefList`
- `src/features/agent-app/runtime/agentUiProjectionBridge.ts` 已拆出 `agentUiProjectionFieldReaders.ts``agentUiRuntimeEventAdapter.ts`,主文件从 `1253` 行降到 `895` 行,低于 `1000` 行治理红线;相邻 Agent App projection 回归通过。
- `src/features/agent-app/runtime/agentUiProjectionBridge.ts` 已继续压薄为 `82` 行 normalization facadeprojection builders / owner-scope-status mapping 分别进入 `agentUiProjectionBuilders.ts``agentUiProjectionMapping.ts`;相关文件均低于 `800` 行预警线。
- `src/features/agent-app/runtime/agentUiProjectionBoundary.test.ts` 已补结构守卫,防止 bridge 重新承接 builders / mapping 职责,并机械检查 projection runtime 拆分文件低于 `800` 行。
- `src/features/agent-app/ui/AgentRunHostDrawer.tsx` 已拆出 `AgentRunHostDrawerProjectionInput.ts` 并压薄为标准 projection 宿主壳;主抽屉始终渲染 `AgentRunProjectionPanel`,不再 import 或调用本地 process fallback,标准 projection 负责空态。
- `src/features/agent-app/ui/AgentRunHostDrawerFallback.tsx` 文件仍存在,但已退出主路径,只作为待删除的 `deprecated` residual;物理删除需单独确认。
- `src/features/agent-app/ui/AgentRunHostDrawerBoundary.test.ts` 已补结构守卫,防止主抽屉直接 import 本地 process 组件、fallback 文件或旧条件分支,并要求空态交给标准 projection。
- `/Users/coso/Documents/dev/ai/limecloud/lime-agent-workbench/docs/tutorials/content-studio.md` 已补完整 adoption blueprint,明确 Content Studio owns / does not own、标准 surface 映射、compat 迁移表和 blocked 行为。
- `/Users/coso/Documents/dev/ai/limecloud/content-studio/src/renderer/src/components/agent/AgentUiProjectionSurface.tsx` 已作为产品侧标准 surface adapter,统一组合 `@limecloud/agent-runtime-ui` 的 timeline / facts primitives 与 Content Studio artifact/evidence refs。
- Content Studio `AgentsWorkbench` / `AgentSessionPanel` 已改为消费 `AgentUiProjectionSurface`,不再直接散装组合共享 AgentUI primitives。
- `/Users/coso/Documents/dev/ai/limecloud/content-studio/scripts/lime-agent-boundary-audit.mjs` 已增加边界检查,阻止产品页面绕过标准 surface 直接拼共享 primitives,并检查 `.agent-ui-projection` / `.agent-ui-main` / `.agent-ui-sidecar` DOM contract。
- `/Users/coso/Documents/dev/ai/limecloud/content-studio/src/main/services/appServerAgentRuntimeGateway.ts` 已抽出 `ContentStudioAgentRuntimeSessionGateway`,并通过 `@limecloud/agent-runtime-client/sessionGateway``createAgentRuntimeClientFromSessionGateway(...)` 包装成标准 `AgentRuntimeClient`;本地 gateway 只保留 `agentSession/start``artifact/read` 和 sidecar transport 适配。
- `ContentStudioAgentRuntimeSessionGateway.nextEvent()` 已保持标准 `agentSession/event` notification 形状,内部 turn drain 通过标准 `runtimeClient.nextEvent()` 消费 notification,再由本地 guard 提取裸 `RuntimeEvent`
- 标准包发布前门槛已通过:`npm --prefix packages/app-server-client run test``npm --prefix packages/agent-runtime-client run test``cargo test --manifest-path "lime-rs/Cargo.toml" -p app-server-protocol schema_fixtures_match_generated_output`、两个 package 目录内的 `npm pack --dry-run --json --ignore-scripts` 均通过。
- `npm view "@limecloud/app-server-client@1.66.0" version``npm view "@limecloud/agent-runtime-client@0.1.1" version` 已可查询;`npm view "app-server-client@1.66.0" version``npm view "@limecloud/agent-runtime-client@0.1.0" version` 只作为误发兼容记录。
- [standard-package-release-runbook.md](standard-package-release-runbook.md) 已更新为 scoped registry 发布、Content Studio 接入、GUI smoke 验收和 compat-misrelease 处理证据。
## Workstream 5React Artifact / Evidence Refs
| 项 | 内容 |
| --- | --- |
| 状态 | done |
| 写集 | `packages/agent-runtime-ui/**`、Workbench runtime-ui docs |
| current owner | `@limecloud/agent-runtime-ui` 只渲染 `state.artifacts` / `state.evidence` 轻量 refs,并通过 callback 交还宿主。 |
| 禁止方向 | 在 React 包里读取 artifact body、导出 evidence pack、推断 review verdict 或复制大 payload。 |
完成证据:
- `packages/agent-runtime-ui/src/refs.tsx` 新增 `AgentUiRefList``ArtifactRefList``EvidenceRefList`
- `AgentUiProjectionView` 接入 `onSelectArtifactRef` / `onSelectEvidenceRef` 和 refs labels。
- `packages/agent-runtime-ui/tests/ui.test.mjs` 覆盖 artifact/evidence fixture render 和稳定 DOM contract。
- `npm --prefix packages/agent-runtime-ui run test` 通过。
- `src/features/agent-app/runtime/agentRunProjectionState.ts` 使用 `projectAgentUiState` 生成标准 `AgentUiProjectionState`
- 定向测试 `agentRuntimeClientApi``agentRuntimeAppServerClient``agentRuntimeCapabilityHost``AgentAppRuntimePage.hostBridge``AgentRunProjectionPanel` 已通过。
剩余:
- Content Studio 已安装并接入 `@limecloud/agent-runtime-client/sessionGateway``package-lock.json` 解析到 `@limecloud/agent-runtime-client@0.1.1` 与间接 `@limecloud/app-server-client@1.66.0` registry tarball。
- Content Studio 仍缺真实 GUI smoke / 产品回归证据。
- Agent App 本地 process fallback 文件仍存在但已退出主路径;不得再把它接回主抽屉或扩成主展示 owner。
- Agent App 后续新增协议映射必须进入 builders / mapping / field readers 对应边界,不得塞回 `agentUiProjectionBridge.ts`
- Agent App 主抽屉后续新增 UI 必须继续走标准 projection panel 或独立宿主壳;空态也必须交给标准 projection。
## Workstream 5SDK React Surfaces Adoption
| 项 | 内容 |
| --- | --- |
| 状态 | done |
| 写集 | `packages/agent-runtime-ui/**`,必要时只读产品应用现有过程组件。 |
| current owner | `@limecloud/agent-runtime-ui` 只消费 projection state 和 command callbacks。 |
| 禁止方向 | React surface 直接订阅 Provider、发 JSON-RPC、持有 runtime facts。 |
任务:
- 固定 MessageParts、ProcessTimeline、ExecutionGraph、ActionRequired、Artifact / Evidence surfaces 的 props contract。
- 把复杂状态分支保留在 projection / selectorsReact 测试只覆盖渲染与事件接线。
- 准备产品应用可复用的最小 surface 示例,但不把业务逻辑塞进 UI 包。
验证:
```bash
npm --prefix packages/agent-runtime-ui run test
npm --prefix packages/agent-runtime-ui run typecheck
```
完成证据:
- React surfaces 不再依赖产品本地 runtime state。
- fixture smoke 覆盖核心 surfaces。
- `SubagentsView` 只消费 `state.subagents`,不在组件内重新筛 `graph` / `readModel`
- `RuntimeFactCard` 输出稳定 `agent-runtime-event` CSS contract。
- `iteration-plan.md` 追加日志。
## Workstream 6Release Integration
| 项 | 内容 |
| --- | --- |
| 状态 | blocked |
| 阻塞 | 需要用户明确要求版本号、commit、tag、push 或 GitHub Pages 发布。 |
| 写集 | Workbench `package.json``package-lock.json``docs/development/updates.md`、GitHub workflow;必要时版本 tag。 |
| current owner | GitHub Pages workflow + Workbench package version。 |
任务:
- 只有在用户明确要求后,才更新版本号并发布。
- 发布前运行 docs build。
- 若进入 Lime 主仓版本相关文件,必须按仓库发版规则运行版本一致性检查。
验证:
```bash
cd /Users/coso/Documents/dev/ai/limecloud/lime-agent-workbench
npm run docs:build
```
完成证据:
- 版本、更新记录、构建、tag / push 状态一致。
- 发布行为已由用户明确授权。
## Workstream 7Standard Package Release / Content Studio Runtime Client Adoption
| 项 | 内容 |
| --- | --- |
| 状态 | in-progress |
| 阻塞 | scoped registry 发布和 Content Studio 依赖接入已完成;剩余阻塞是 Content Studio GUI smoke / 产品回归。 |
| 写集 | 发布阶段优先 `packages/app-server-client/**``packages/agent-runtime-client/**`;接入阶段另行认领 `/Users/coso/Documents/dev/ai/limecloud/content-studio/package.json`、lockfile、`src/main/services/appServerAgentRuntimeGateway.ts``scripts/lime-agent-boundary-audit.mjs`。 |
| current owner | npm registry 正式包 + Content Studio 标准 runtime-client facade。 |
| 禁止方向 | `file:` / 绝对路径依赖、复制 dist、把 Content Studio 本地 gateway 继续扩成第二套 runtime client。 |
任务:
- 维持 [standard-package-release-runbook.md](standard-package-release-runbook.md) 中的 scoped registry 事实源。
- 保持 Content Studio 固定安装并消费 `@limecloud/agent-runtime-client@0.1.1/sessionGateway`
- 补 Content Studio GUI smoke / 产品回归。
验证:
```bash
npm view "@limecloud/app-server-client@1.66.0" version
npm view "@limecloud/agent-runtime-client@0.1.1" version
```
Content Studio 接入后再运行:
```bash
npm run verify:lime-agent
npm run test:functional -- --test-name-pattern "Lime Agent 边界审计会阻断 runtime/key/UI 协议回流"
npx tsc --noEmit --pretty false
```
完成证据:
- 两个目标包均可从 npm registry 查询。
- Content Studio lockfile 消费正式 registry 包。
- Content Studio 主路径实际调用 `@limecloud/agent-runtime-client/sessionGateway`
- `npm run verify:lime-agent`、定向 functional 和 `npx tsc --noEmit --pretty false` 已通过。
- GUI smoke 证明 `.agent-ui-projection` 标准 surface 可运行。
## 当前推荐下一刀
优先级按对整体目标的提升排序:
1. Content Studio GUI smoke / 产品回归。证明 `.agent-ui-projection` 标准 surface 在真实页面中可运行,且边界审计进入常规验证。
2. Agent App 本地 process fallback 物理删除。`AgentRunHostDrawerFallback.tsx` 已退出主路径并有结构守卫防回流;删除文件属于高风险文件系统操作,需要单独确认后执行。
3. 真实 Provider / live evidence。只有在明确授权真实 Provider 后,补 live streaming / evidence 验证;日常回归继续使用 current fixture 和 GUI smoke。
不要优先做发布集成;当前 v0.3.0 / v0.4.0 仍缺 Content Studio 真实产品主路径和必要的收口拆分,发布只能算阶段快照。
+73
View File
@@ -0,0 +1,73 @@
# Subagents 路线图
## 目标
`/subagents` 对齐成熟终端代理的 subagents 行为:它是本地 slash command,用于启用、展示或切换子代理线程状态;它不是 Team 选择器,不是任务分工弹窗,也不是协作画布 / dock / workbench。
参考事实:
- `/subagents` 是本地 TUI 事件,不把命令文本发给模型。
- `/agent``/subagents` 都进入本地 agent pickerpicker 标题使用 `Subagents`
- Subagents 导航只维护 thread entry、稳定顺序、label、running/closed 状态和 active thread,不提供 Team profile 配置 UI。
## 当前事实源
### current
- 聊天输入中的 `/subagents` 是本地 slash action,命中后只调用 `onOpenSubagents`,不发送给模型,不走 mock,不新增 Electron IPC 或 App Server JSON-RPC 命令。
- 输入栏 plus menu 的 Subagents 状态只表达当前 turn 可使用 Subagents/thread runtime,不打开 Team picker、Team profile editor 或协作工作台。
- Subagents 运行态展示继续收敛到 runtime projection / workspace scene runtime
- `src/components/agent/chat/team-workspace-runtime/liveRuntimeProjector.ts`
- `src/components/agent/chat/team-workspace-runtime/runtimeEventSubscriptions.ts`
- `src/components/agent/chat/team-workspace-runtime/runtimeStateReconciler.ts`
- `src/components/agent/chat/workspace/useWorkspaceTeamSessionRuntime.ts`
- App Server / RuntimeCore 后续若提供 first-class subagent thread list,前端只允许接到 Subagents/thread projection,不得恢复 Team 配置面。
### compat
- `TeamDefinition``selectedTeam``recent_team_selection``teamWorkspaceRuntime``useTeamWorkspaceRuntime``lime.chat.team_selection.v1.general` 仍是历史兼容命名,只能承载现有 runtime metadata / session projection。
- 兼容字段可以继续被发送边界读取,但不允许重新变成用户可见 Team 概念、profile 编辑器、任务分工模板或自定义 Team 存储入口。
- 退出条件:App Server / RuntimeCore 提供 current `SubagentThread` / `SubagentProfile` 事实源后,将上述 compat 命名迁成 subagent/thread 语义,并删除不再被 current projection 使用的存储 key。
### dead
- `TeamSelector``TeamSelectorPanel``inputbarTeamSelectorCopy`
- `TeamSuggestionBar``teamSuggestion` 主动评分推荐条、`onEnableSuggestedTeam` / `handleEnableSuggestedTeam` / `enableSuggestedTeam` 旧 preset 写入口。
- Team 选择弹窗、Team profile editor、自定义分工的新建 / 编辑 / 删除 / 复制入口。
- 自定义 Team 克隆 / 本地写入入口:`cloneTeamDefinitionAsCustom``saveCustomTeams``buildWorkspaceSettingsWithCustomTeams``lime.chat.custom_teams.v1`
- Team 画布、Team workspace board、Team dock 与 Team summary panel。
- Team memory shadow card / panel、AgentUI 旧协作操作 surface 以及 `team-workspace-board/` 整个目录。
- `empty-state-team-selector``team-selector-stub` 等旧 test id / DOM surface。
- `agentChat.inputbar.teamSelector.*` 与 Team canvas / board / dock / formation / selectedSession / activityPreview / operations / canvasLane / teamPreset i18n key。
- `agentChat.inputbar.teamSuggestion.*` i18n key 与 `team-suggestion-bar` DOM surface。
## 实施约束
1. `/subagents` 必须保持本地 command:执行后返回 handled,不调用 `rawSendMessage`,不把 `/subagents` 文本送入模型。
2. UI 只展示 Subagents/thread/runtime 状态;不得弹 TeamSelector Popover,不得打开 TeamSelectorPanel,不得出现 Team 画布或旧协作工作台。
3. 新增用户可见文案必须使用 Subagents / 子代理语义,并覆盖 `zh-CN / zh-TW / en-US / ja-JP / ko-KR`
4. 治理守卫必须阻止旧 Team UI 文件、路径、props、test id 和 i18n key 回流。
5. 保留的 `team*` compat 命名必须标明用途和退出条件,不能继续承接新功能。
## 本轮实施切口
1. 删除 Team selector/panel/canvas/workbench/dock 用户可见实现和测试。
2. `/subagents` 接到本地 executor 与输入栏 Subagents 状态,不再走弹窗式 Team 配置。
3. 清理 Team selector/canvas/workbench i18n key 与组件索引。
4. 补治理目录册和质量脚本守卫,防止旧 Team UI surface 回流。
5. 用定向单测、i18n 测试、治理报告和 contract 测试验证本地 command 与旧入口清退。
## 2026-06-10 实施记录
- 已删除 `TeamSelector` / `TeamSelectorPanel` / `inputbarTeamSelectorCopy``/subagents` 保持本地 command,不再触发任务分工弹窗。
- 已删除 Team 画布、Team workspace board、dock、summary panel、memory shadow card 和 `team-workspace-board/` 目录;Subagents 展示回到 runtime projection / thread 状态主链。
- 已删除 `TeamSuggestionBar``teamSuggestion` 主动推荐链;复杂输入不再弹出或插入推荐条,Subagents 只通过显式 `/subagents`、工具状态和线程展示进入。
- 已删除未使用的自定义 Team 克隆与本地 `custom_teams` 写入入口;workspace settings 中的历史 `customTeams` 只保留只读 compat 解析。
- 已把用户可见文案从 `Team current tools``Team Memory``Team member``teammate` 等改为 Subagents / 子代理语义;兼容 key 仅保留在协议、storage 和 runtime metadata 边界。
- 已补 `legacySurfaceCatalog` dead surface 守卫,覆盖旧文件、路径、props、test id、Team 画布术语和旧展示文案,防止后续恢复。
## 后续退出条件
- Runtime 提供 first-class Subagents thread/profile schema 后,迁掉 `TeamDefinition` / `selectedTeam` / `recent_team_selection` 等 compat 命名。
- `agentTeamWorkspace` namespace 只剩 current runtimeStatus / liveRuntime / control 文案后,评估是否改名为 Subagents namespace。
- 若 `teamMemorySnapshot` 仍只作为历史 metadata 展示,保持只读 compat;若无 current 消费,按 `dead` 删除并补守卫。
+23 -23
View File
@@ -161,7 +161,7 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "app-server"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"anyhow",
"app-server-protocol",
@@ -201,7 +201,7 @@ dependencies = [
[[package]]
name = "app-server-client"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"app-server-protocol",
"app-server-transport",
@@ -212,7 +212,7 @@ dependencies = [
[[package]]
name = "app-server-daemon"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"app-server-protocol",
"app-server-transport",
@@ -224,7 +224,7 @@ dependencies = [
[[package]]
name = "app-server-protocol"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"schemars",
"serde",
@@ -233,7 +233,7 @@ dependencies = [
[[package]]
name = "app-server-test-client"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"anyhow",
"app-server-client",
@@ -244,7 +244,7 @@ dependencies = [
[[package]]
name = "app-server-transport"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"app-server-protocol",
"serde_json",
@@ -3903,7 +3903,7 @@ dependencies = [
[[package]]
name = "lime-agent"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"anyhow",
"aster-core",
@@ -3932,7 +3932,7 @@ dependencies = [
[[package]]
name = "lime-browser-runtime"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"chrono",
"futures",
@@ -3949,7 +3949,7 @@ dependencies = [
[[package]]
name = "lime-cli"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"clap",
"lime-core",
@@ -3961,7 +3961,7 @@ dependencies = [
[[package]]
name = "lime-config"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"async-trait",
"lime-core",
@@ -3977,7 +3977,7 @@ dependencies = [
[[package]]
name = "lime-core"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"aster-models",
"async-trait",
@@ -4033,7 +4033,7 @@ dependencies = [
[[package]]
name = "lime-gateway"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"aes",
"axum 0.7.9",
@@ -4063,7 +4063,7 @@ dependencies = [
[[package]]
name = "lime-infra"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"chrono",
"dashmap 5.5.3",
@@ -4083,7 +4083,7 @@ dependencies = [
[[package]]
name = "lime-knowledge"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"chrono",
"hex",
@@ -4096,7 +4096,7 @@ dependencies = [
[[package]]
name = "lime-mcp"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"async-trait",
"dirs 5.0.1",
@@ -4112,7 +4112,7 @@ dependencies = [
[[package]]
name = "lime-media-runtime"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"axum 0.7.9",
"base64 0.22.1",
@@ -4144,7 +4144,7 @@ dependencies = [
[[package]]
name = "lime-processor"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"async-trait",
"lime-core",
@@ -4163,7 +4163,7 @@ dependencies = [
[[package]]
name = "lime-providers"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"anyhow",
"async-stream",
@@ -4218,7 +4218,7 @@ dependencies = [
[[package]]
name = "lime-server"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"aster-core",
"async-stream",
@@ -4262,7 +4262,7 @@ dependencies = [
[[package]]
name = "lime-server-utils"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"axum 0.7.9",
"futures",
@@ -4277,7 +4277,7 @@ dependencies = [
[[package]]
name = "lime-services"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"anyhow",
"aster-core",
@@ -4320,7 +4320,7 @@ dependencies = [
[[package]]
name = "lime-skills"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"async-trait",
"dirs 5.0.1",
@@ -4338,7 +4338,7 @@ dependencies = [
[[package]]
name = "lime-websocket"
version = "1.65.0"
version = "1.66.0"
dependencies = [
"axum 0.7.9",
"chrono",
+1 -1
View File
@@ -11,7 +11,7 @@ exclude = [
resolver = "2"
[workspace.package]
version = "1.65.0"
version = "1.66.0"
edition = "2021"
authors = ["coso"]
repository = "https://github.com/aiclientproxy/lime"
@@ -200,6 +200,8 @@ pub use app_server_protocol::WechatConfiguredAccount;
pub use app_server_protocol::WindowsStartupCheck;
pub use app_server_protocol::WindowsStartupDiagnosticsResponse;
pub use app_server_protocol::WorkspaceEnsureParams;
pub use app_server_protocol::WorkspaceEnsureProjectParams;
pub use app_server_protocol::WorkspaceEnsureProjectResponse;
pub use app_server_protocol::WorkspaceEnsureReadyResponse;
pub use app_server_protocol::WorkspaceListResponse;
pub use app_server_protocol::WorkspacePathReadParams;
@@ -347,6 +349,7 @@ pub use app_server_protocol::METHOD_WECHAT_CHANNEL_ACCOUNT_LIST;
pub use app_server_protocol::METHOD_WORKSPACE_BY_PATH_READ;
pub use app_server_protocol::METHOD_WORKSPACE_DEFAULT_ENSURE;
pub use app_server_protocol::METHOD_WORKSPACE_DEFAULT_READ;
pub use app_server_protocol::METHOD_WORKSPACE_ENSURE;
pub use app_server_protocol::METHOD_WORKSPACE_ENSURE_READY;
pub use app_server_protocol::METHOD_WORKSPACE_LIST;
pub use app_server_protocol::METHOD_WORKSPACE_PROJECTS_ROOT_READ;
@@ -516,6 +519,13 @@ impl AppServerClient {
self.typed_request(typed::read_workspace_by_path(params))
}
pub fn ensure_workspace(
&mut self,
params: WorkspaceEnsureProjectParams,
) -> Result<JsonRpcRequest, ClientError> {
self.typed_request(typed::ensure_workspace(params))
}
pub fn read_default_workspace(&mut self) -> Result<JsonRpcRequest, ClientError> {
self.typed_request(typed::read_default_workspace())
}
@@ -1352,6 +1362,12 @@ pub mod typed {
TypedRequest::new(METHOD_WORKSPACE_BY_PATH_READ, params)
}
pub fn ensure_workspace(
params: WorkspaceEnsureProjectParams,
) -> TypedRequest<WorkspaceEnsureProjectParams> {
TypedRequest::new(METHOD_WORKSPACE_ENSURE, params)
}
pub fn read_default_workspace() -> TypedRequest<serde_json::Value> {
TypedRequest::new(METHOD_WORKSPACE_DEFAULT_READ, serde_json::json!({}))
}
@@ -2408,6 +2424,13 @@ mod tests {
root_path: "/workspace/project".to_string(),
})
.expect("workspace by path");
let ensured_workspace = client
.ensure_workspace(WorkspaceEnsureProjectParams {
name: "content-studio".to_string(),
root_path: "/workspace/content-studio".to_string(),
workspace_type: Some("general".to_string()),
})
.expect("ensure workspace");
let default_workspace = client.read_default_workspace().expect("default workspace");
let ensured_default = client
.ensure_default_workspace()
@@ -2439,6 +2462,15 @@ mod tests {
workspace_by_path.params.expect("params"),
json!({ "rootPath": "/workspace/project" })
);
assert_eq!(ensured_workspace.method, METHOD_WORKSPACE_ENSURE);
assert_eq!(
ensured_workspace.params.expect("params"),
json!({
"name": "content-studio",
"rootPath": "/workspace/content-studio",
"workspaceType": "general",
})
);
assert_eq!(default_workspace.method, METHOD_WORKSPACE_DEFAULT_READ);
assert_eq!(ensured_default.method, METHOD_WORKSPACE_DEFAULT_ENSURE);
assert_eq!(projects_root.method, METHOD_WORKSPACE_PROJECTS_ROOT_READ);
@@ -3411,6 +3443,7 @@ mod tests {
assert!(methods.contains(&METHOD_AGENT_APP_SHELL_PREPARE));
assert!(methods.contains(&METHOD_WORKSPACE_READ));
assert!(methods.contains(&METHOD_WORKSPACE_BY_PATH_READ));
assert!(methods.contains(&METHOD_WORKSPACE_ENSURE));
assert!(methods.contains(&METHOD_WORKSPACE_DEFAULT_READ));
assert!(methods.contains(&METHOD_WORKSPACE_DEFAULT_ENSURE));
assert!(methods.contains(&METHOD_WORKSPACE_PROJECTS_ROOT_READ));
@@ -19717,6 +19717,48 @@
"title": "WorkspaceEnsureParams",
"type": "object"
},
"WorkspaceEnsureProjectParams": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"name": {
"type": "string"
},
"rootPath": {
"type": "string"
},
"workspaceType": {
"type": [
"null",
"string"
]
}
},
"required": [
"name",
"rootPath"
],
"title": "WorkspaceEnsureProjectParams",
"type": "object"
},
"WorkspaceEnsureProjectResponse": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"created": {
"type": "boolean"
},
"rootCreated": {
"type": "boolean"
},
"workspace": true
},
"required": [
"created",
"rootCreated",
"workspace"
],
"title": "WorkspaceEnsureProjectResponse",
"type": "object"
},
"WorkspaceEnsureReadyResponse": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
@@ -20197,6 +20239,7 @@
"workspace/byPath/read",
"workspace/default/ensure",
"workspace/default/read",
"workspace/ensure",
"workspace/ensureReady",
"workspace/list",
"workspace/projectPath/resolve",
@@ -922,6 +922,10 @@
"kind": "request",
"method": "workspace/default/read"
},
{
"kind": "request",
"method": "workspace/ensure"
},
{
"kind": "request",
"method": "workspace/ensureReady"
@@ -1385,6 +1389,8 @@
"WindowsStartupCheck",
"WindowsStartupDiagnosticsResponse",
"WorkspaceEnsureParams",
"WorkspaceEnsureProjectParams",
"WorkspaceEnsureProjectResponse",
"WorkspaceEnsureReadyResponse",
"WorkspaceListResponse",
"WorkspacePathReadParams",
@@ -0,0 +1,23 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"name": {
"type": "string"
},
"rootPath": {
"type": "string"
},
"workspaceType": {
"type": [
"null",
"string"
]
}
},
"required": [
"name",
"rootPath"
],
"title": "WorkspaceEnsureProjectParams",
"type": "object"
}
@@ -0,0 +1,19 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"created": {
"type": "boolean"
},
"rootCreated": {
"type": "boolean"
},
"workspace": true
},
"required": [
"created",
"rootCreated",
"workspace"
],
"title": "WorkspaceEnsureProjectResponse",
"type": "object"
}
@@ -70,6 +70,7 @@ pub const APP_SERVER_METHODS: &[AppServerMethodSpec] = &[
method!(METHOD_SESSION_FILE_LIST, Request),
method!(METHOD_WORKSPACE_LIST, Request),
method!(METHOD_WORKSPACE_READ, Request),
method!(METHOD_WORKSPACE_ENSURE, Request),
method!(METHOD_WORKSPACE_BY_PATH_READ, Request),
method!(METHOD_WORKSPACE_DEFAULT_READ, Request),
method!(METHOD_WORKSPACE_DEFAULT_ENSURE, Request),
@@ -43,6 +43,7 @@ pub const METHOD_SESSION_FILE_DELETE: &str = "sessionFile/delete";
pub const METHOD_SESSION_FILE_LIST: &str = "sessionFile/list";
pub const METHOD_WORKSPACE_LIST: &str = "workspace/list";
pub const METHOD_WORKSPACE_READ: &str = "workspace/read";
pub const METHOD_WORKSPACE_ENSURE: &str = "workspace/ensure";
pub const METHOD_WORKSPACE_BY_PATH_READ: &str = "workspace/byPath/read";
pub const METHOD_WORKSPACE_DEFAULT_READ: &str = "workspace/default/read";
pub const METHOD_WORKSPACE_DEFAULT_ENSURE: &str = "workspace/default/ensure";
@@ -89,10 +89,12 @@ pub const V0_SCHEMA_TYPE_NAMES: &[&str] = &[
"SessionFileMutationResponse",
"WorkspaceReadParams",
"WorkspacePathReadParams",
"WorkspaceEnsureProjectParams",
"WorkspaceProjectPathResolveParams",
"WorkspaceEnsureParams",
"WorkspaceListResponse",
"WorkspaceReadResponse",
"WorkspaceEnsureProjectResponse",
"WorkspaceProjectsRootReadResponse",
"WorkspaceProjectPathResolveResponse",
"WorkspaceEnsureReadyResponse",
@@ -47,6 +47,7 @@ fn app_server_method_catalog_keeps_request_and_notification_methods_together() {
METHOD_SESSION_FILE_LIST,
METHOD_WORKSPACE_LIST,
METHOD_WORKSPACE_READ,
METHOD_WORKSPACE_ENSURE,
METHOD_WORKSPACE_BY_PATH_READ,
METHOD_WORKSPACE_DEFAULT_READ,
METHOD_WORKSPACE_DEFAULT_ENSURE,
@@ -13,6 +13,15 @@ pub struct WorkspacePathReadParams {
pub root_path: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceEnsureProjectParams {
pub name: String,
pub root_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace_type: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceProjectPathResolveParams {
@@ -41,6 +50,14 @@ pub struct WorkspaceReadResponse {
pub workspace: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceEnsureProjectResponse {
pub workspace: serde_json::Value,
pub created: bool,
pub root_created: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceProjectsRootReadResponse {
@@ -159,10 +159,12 @@ pub(super) fn v0_schemas() -> Vec<GeneratedJsonSchema> {
typed_schema::<SessionFileMutationResponse>("SessionFileMutationResponse"),
typed_schema::<WorkspaceReadParams>("WorkspaceReadParams"),
typed_schema::<WorkspacePathReadParams>("WorkspacePathReadParams"),
typed_schema::<WorkspaceEnsureProjectParams>("WorkspaceEnsureProjectParams"),
typed_schema::<WorkspaceProjectPathResolveParams>("WorkspaceProjectPathResolveParams"),
typed_schema::<WorkspaceEnsureParams>("WorkspaceEnsureParams"),
typed_schema::<WorkspaceListResponse>("WorkspaceListResponse"),
typed_schema::<WorkspaceReadResponse>("WorkspaceReadResponse"),
typed_schema::<WorkspaceEnsureProjectResponse>("WorkspaceEnsureProjectResponse"),
typed_schema::<WorkspaceProjectsRootReadResponse>("WorkspaceProjectsRootReadResponse"),
typed_schema::<WorkspaceProjectPathResolveResponse>("WorkspaceProjectPathResolveResponse"),
typed_schema::<WorkspaceEnsureReadyResponse>("WorkspaceEnsureReadyResponse"),
@@ -291,6 +291,8 @@ use app_server_protocol::WechatRuntimeModelSetParams;
use app_server_protocol::WechatRuntimeModelSetResponse;
use app_server_protocol::WindowsStartupDiagnosticsResponse;
use app_server_protocol::WorkspaceEnsureParams;
use app_server_protocol::WorkspaceEnsureProjectParams;
use app_server_protocol::WorkspaceEnsureProjectResponse;
use app_server_protocol::WorkspaceEnsureReadyResponse;
use app_server_protocol::WorkspaceListResponse;
use app_server_protocol::WorkspacePathReadParams;
@@ -511,6 +513,13 @@ impl AppDataSource for LocalAppDataSource {
workspaces::read_workspace_by_path(&self.db, params)
}
async fn ensure_project_workspace(
&self,
params: WorkspaceEnsureProjectParams,
) -> Result<WorkspaceEnsureProjectResponse, RuntimeCoreError> {
workspaces::ensure_project_workspace(&self.db, params)
}
async fn read_default_workspace(&self) -> Result<WorkspaceReadResponse, RuntimeCoreError> {
workspaces::read_default_workspace(&self.db)
}
@@ -1,6 +1,8 @@
use super::data_error;
use crate::RuntimeCoreError;
use app_server_protocol::WorkspaceEnsureParams;
use app_server_protocol::WorkspaceEnsureProjectParams;
use app_server_protocol::WorkspaceEnsureProjectResponse;
use app_server_protocol::WorkspaceEnsureReadyResponse;
use app_server_protocol::WorkspaceListResponse;
use app_server_protocol::WorkspacePathReadParams;
@@ -75,6 +77,61 @@ pub(crate) fn read_workspace_by_path(
Ok(WorkspaceReadResponse { workspace })
}
pub(crate) fn ensure_project_workspace(
db: &DbConnection,
params: WorkspaceEnsureProjectParams,
) -> Result<WorkspaceEnsureProjectResponse, RuntimeCoreError> {
let conn = database::lock_db(db).map_err(data_error)?;
let root = PathBuf::from(params.root_path.trim());
if params.root_path.trim().is_empty() {
return Err(data_error("workspace root_path is required"));
}
let root_path = root
.to_str()
.ok_or_else(|| data_error("invalid workspace root_path"))?
.to_string();
if let Some(existing) =
read_workspace_by_root_path(&conn, Path::new(&root_path)).map_err(data_error)?
{
let existed = root.is_dir();
fs::create_dir_all(&root).map_err(data_error)?;
return Ok(WorkspaceEnsureProjectResponse {
workspace: existing,
created: false,
root_created: !existed,
});
}
let workspace_type = normalize_workspace_type(params.workspace_type.as_deref())?;
let name = normalize_workspace_name(&params.name, &root_path);
let existed = root.is_dir();
fs::create_dir_all(&root).map_err(data_error)?;
let now = Utc::now().timestamp_millis();
let id = Uuid::new_v4().to_string();
let icon = default_workspace_icon(workspace_type);
conn.execute(
"INSERT INTO workspaces (
id, name, workspace_type, root_path, is_default, settings_json,
icon, color, is_favorite, is_archived, tags_json, default_persona_id,
created_at, updated_at
)
VALUES (?, ?, ?, ?, 0, '{}', ?, NULL, 0, 0, '[]', NULL, ?, ?)",
params![id, name, workspace_type, root_path, icon, now, now],
)
.map_err(|error| data_error(format!("ensure workspace failed: {error}")))?;
let workspace = read_workspace_by_id(&conn, &id)
.map_err(data_error)?
.ok_or_else(|| data_error("failed to reload ensured workspace"))?;
Ok(WorkspaceEnsureProjectResponse {
workspace,
created: true,
root_created: !existed,
})
}
pub(crate) fn read_default_workspace(
db: &DbConnection,
) -> Result<WorkspaceReadResponse, RuntimeCoreError> {
@@ -314,6 +371,41 @@ fn workspace_id(value: &Value) -> Option<&str> {
value.get("id").and_then(Value::as_str)
}
fn normalize_workspace_type(value: Option<&str>) -> Result<&'static str, RuntimeCoreError> {
match value.map(str::trim).filter(|value| !value.is_empty()) {
None => Ok("general"),
Some("persistent") => Ok("persistent"),
Some("temporary") => Ok("temporary"),
Some("general") => Ok("general"),
Some(value) => Err(data_error(format!(
"unsupported workspace_type '{value}', only persistent / temporary / general are supported"
))),
}
}
fn normalize_workspace_name(name: &str, root_path: &str) -> String {
let trimmed = name.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
Path::new(root_path)
.file_name()
.and_then(|value| value.to_str())
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("未命名项目")
.to_string()
}
fn default_workspace_icon(workspace_type: &str) -> Option<&'static str> {
match workspace_type {
"general" => Some("💬"),
"persistent" | "temporary" => Some("📁"),
_ => None,
}
}
fn sanitize_project_dir_name(name: &str) -> String {
let sanitized: String = name
.trim()
+23 -6
View File
@@ -126,12 +126,12 @@ use app_server_protocol::ModelProviderUiStateReadParams;
use app_server_protocol::ModelProviderUiStateWriteParams;
use app_server_protocol::ModelProviderUpdateParams;
use app_server_protocol::PlatformInfo;
use app_server_protocol::ProjectMemoryReadParams;
use app_server_protocol::ProjectMaterialImportFromUrlParams;
use app_server_protocol::ProjectMaterialListParams;
use app_server_protocol::ProjectMaterialLookupParams;
use app_server_protocol::ProjectMaterialUpdateParams;
use app_server_protocol::ProjectMaterialUploadParams;
use app_server_protocol::ProjectMemoryReadParams;
use app_server_protocol::ServerCapabilities;
use app_server_protocol::ServerInfo;
use app_server_protocol::SessionFileGetOrCreateParams;
@@ -156,7 +156,6 @@ use app_server_protocol::SkillRemoteInspectParams;
use app_server_protocol::SkillRepositoryDeleteParams;
use app_server_protocol::SkillRepositorySaveParams;
use app_server_protocol::SkillScaffoldCreateParams;
use app_server_protocol::UsageStatsRangeParams;
use app_server_protocol::UnifiedMemoryAnalyzeParams;
use app_server_protocol::UnifiedMemoryCreateParams;
use app_server_protocol::UnifiedMemoryDeleteParams;
@@ -166,6 +165,7 @@ use app_server_protocol::UnifiedMemoryListParams;
use app_server_protocol::UnifiedMemorySearchParams;
use app_server_protocol::UnifiedMemorySemanticSearchParams;
use app_server_protocol::UnifiedMemoryUpdateParams;
use app_server_protocol::UsageStatsRangeParams;
use app_server_protocol::VoiceAsrCredentialCreateParams;
use app_server_protocol::VoiceAsrCredentialIdParams;
use app_server_protocol::VoiceAsrCredentialUpdateParams;
@@ -178,6 +178,7 @@ use app_server_protocol::WechatLoginStartParams;
use app_server_protocol::WechatLoginWaitParams;
use app_server_protocol::WechatRuntimeModelSetParams;
use app_server_protocol::WorkspaceEnsureParams;
use app_server_protocol::WorkspaceEnsureProjectParams;
use app_server_protocol::WorkspacePathReadParams;
use app_server_protocol::WorkspaceProjectPathResolveParams;
use app_server_protocol::WorkspaceReadParams;
@@ -339,7 +340,6 @@ use app_server_protocol::METHOD_MODEL_PROVIDER_UI_STATE_READ;
use app_server_protocol::METHOD_MODEL_PROVIDER_UI_STATE_WRITE;
use app_server_protocol::METHOD_MODEL_PROVIDER_UPDATE;
use app_server_protocol::METHOD_MODEL_SYNC_STATE_READ;
use app_server_protocol::METHOD_PROJECT_MEMORY_READ;
use app_server_protocol::METHOD_PROJECT_MATERIAL_CONTENT;
use app_server_protocol::METHOD_PROJECT_MATERIAL_COUNT;
use app_server_protocol::METHOD_PROJECT_MATERIAL_DELETE;
@@ -348,6 +348,7 @@ use app_server_protocol::METHOD_PROJECT_MATERIAL_IMPORT_FROM_URL;
use app_server_protocol::METHOD_PROJECT_MATERIAL_LIST;
use app_server_protocol::METHOD_PROJECT_MATERIAL_UPDATE;
use app_server_protocol::METHOD_PROJECT_MATERIAL_UPLOAD;
use app_server_protocol::METHOD_PROJECT_MEMORY_READ;
use app_server_protocol::METHOD_SESSION_FILE_DELETE;
use app_server_protocol::METHOD_SESSION_FILE_GET_OR_CREATE;
use app_server_protocol::METHOD_SESSION_FILE_LIST;
@@ -378,9 +379,6 @@ use app_server_protocol::METHOD_SKILL_REPOSITORY_DELETE;
use app_server_protocol::METHOD_SKILL_REPOSITORY_LIST;
use app_server_protocol::METHOD_SKILL_REPOSITORY_SAVE;
use app_server_protocol::METHOD_TELEGRAM_CHANNEL_PROBE;
use app_server_protocol::METHOD_USAGE_STATS_DAILY_TRENDS_LIST;
use app_server_protocol::METHOD_USAGE_STATS_MODEL_RANKING_LIST;
use app_server_protocol::METHOD_USAGE_STATS_READ;
use app_server_protocol::METHOD_UNIFIED_MEMORY_ANALYZE;
use app_server_protocol::METHOD_UNIFIED_MEMORY_CREATE;
use app_server_protocol::METHOD_UNIFIED_MEMORY_DELETE;
@@ -391,6 +389,9 @@ use app_server_protocol::METHOD_UNIFIED_MEMORY_SEARCH;
use app_server_protocol::METHOD_UNIFIED_MEMORY_SEMANTIC_SEARCH;
use app_server_protocol::METHOD_UNIFIED_MEMORY_STATS;
use app_server_protocol::METHOD_UNIFIED_MEMORY_UPDATE;
use app_server_protocol::METHOD_USAGE_STATS_DAILY_TRENDS_LIST;
use app_server_protocol::METHOD_USAGE_STATS_MODEL_RANKING_LIST;
use app_server_protocol::METHOD_USAGE_STATS_READ;
use app_server_protocol::METHOD_VOICE_ASR_CREDENTIAL_CREATE;
use app_server_protocol::METHOD_VOICE_ASR_CREDENTIAL_DEFAULT_SET;
use app_server_protocol::METHOD_VOICE_ASR_CREDENTIAL_DELETE;
@@ -411,6 +412,7 @@ use app_server_protocol::METHOD_WECHAT_CHANNEL_RUNTIME_MODEL_SET;
use app_server_protocol::METHOD_WORKSPACE_BY_PATH_READ;
use app_server_protocol::METHOD_WORKSPACE_DEFAULT_ENSURE;
use app_server_protocol::METHOD_WORKSPACE_DEFAULT_READ;
use app_server_protocol::METHOD_WORKSPACE_ENSURE;
use app_server_protocol::METHOD_WORKSPACE_ENSURE_READY;
use app_server_protocol::METHOD_WORKSPACE_LIST;
use app_server_protocol::METHOD_WORKSPACE_PROJECTS_ROOT_READ;
@@ -545,6 +547,7 @@ impl RequestProcessor {
METHOD_AGENT_SESSION_READ => self.handle_session_read(params).await,
METHOD_WORKSPACE_LIST => self.handle_workspace_list().await,
METHOD_WORKSPACE_READ => self.handle_workspace_read(params).await,
METHOD_WORKSPACE_ENSURE => self.handle_workspace_ensure(params).await,
METHOD_WORKSPACE_BY_PATH_READ => self.handle_workspace_by_path_read(params).await,
METHOD_WORKSPACE_DEFAULT_READ => self.handle_workspace_default_read().await,
METHOD_WORKSPACE_DEFAULT_ENSURE => self.handle_workspace_default_ensure().await,
@@ -1313,6 +1316,20 @@ impl RequestProcessor {
dispatch_result(response)
}
async fn handle_workspace_ensure(
&self,
params: Option<serde_json::Value>,
) -> Result<RpcDispatch, JsonRpcError> {
self.ensure_initialized()?;
let params: WorkspaceEnsureProjectParams = parse_params(params)?;
let response = self
.runtime
.ensure_project_workspace(params)
.await
.map_err(to_jsonrpc_error)?;
dispatch_result(response)
}
async fn handle_workspace_default_read(&self) -> Result<RpcDispatch, JsonRpcError> {
self.ensure_initialized()?;
let response = self
+27
View File
@@ -347,6 +347,8 @@ use app_server_protocol::WechatRuntimeModelSetParams;
use app_server_protocol::WechatRuntimeModelSetResponse;
use app_server_protocol::WindowsStartupDiagnosticsResponse;
use app_server_protocol::WorkspaceEnsureParams;
use app_server_protocol::WorkspaceEnsureProjectParams;
use app_server_protocol::WorkspaceEnsureProjectResponse;
use app_server_protocol::WorkspaceEnsureReadyResponse;
use app_server_protocol::WorkspaceListResponse;
use app_server_protocol::WorkspacePathReadParams;
@@ -638,6 +640,15 @@ pub trait AppDataSource: Send + Sync {
params: WorkspacePathReadParams,
) -> Result<WorkspaceReadResponse, RuntimeCoreError>;
async fn ensure_project_workspace(
&self,
_params: WorkspaceEnsureProjectParams,
) -> Result<WorkspaceEnsureProjectResponse, RuntimeCoreError> {
Err(RuntimeCoreError::Backend(
"workspace/ensure is not available without an app data source".to_string(),
))
}
async fn read_default_workspace(&self) -> Result<WorkspaceReadResponse, RuntimeCoreError>;
async fn ensure_default_workspace(&self) -> Result<WorkspaceReadResponse, RuntimeCoreError>;
@@ -2306,6 +2317,15 @@ impl AppDataSource for NoopAppDataSource {
Ok(WorkspaceReadResponse::default())
}
async fn ensure_project_workspace(
&self,
_params: WorkspaceEnsureProjectParams,
) -> Result<WorkspaceEnsureProjectResponse, RuntimeCoreError> {
Err(RuntimeCoreError::Backend(
"workspace/ensure is not available without an app data source".to_string(),
))
}
async fn read_default_workspace(&self) -> Result<WorkspaceReadResponse, RuntimeCoreError> {
Ok(WorkspaceReadResponse::default())
}
@@ -4494,6 +4514,13 @@ impl RuntimeCore {
self.app_data_source.read_workspace_by_path(params).await
}
pub async fn ensure_project_workspace(
&self,
params: WorkspaceEnsureProjectParams,
) -> Result<WorkspaceEnsureProjectResponse, RuntimeCoreError> {
self.app_data_source.ensure_project_workspace(params).await
}
pub async fn read_default_workspace(&self) -> Result<WorkspaceReadResponse, RuntimeCoreError> {
self.app_data_source.read_default_workspace().await
}
+11 -12
View File
@@ -22,18 +22,17 @@ pub use path_utils::{collapse_tilde, contains_tilde, expand_tilde};
pub use types::{
generate_secure_api_key, AmpConfig, AmpModelMapping, ApiKeyEntry, AsrCredentialEntry,
AsrProviderType, AutomationExecutionMode, AutomationSettings, BaiduConfig, ChannelsConfig,
ChatAppearanceConfig, CloudflareTunnelConfig, CompanionDefaultsConfig, Config,
ConversationSettings, CrashReportingConfig, CredentialEntry, CredentialPoolConfig,
CustomProviderConfig, DeliveryConfig, DiscordAccountConfig, DiscordActionsConfig,
DiscordAgentComponentsConfig, DiscordAutoPresenceConfig, DiscordBotConfig,
DiscordChannelConfig, DiscordExecApprovalsConfig, DiscordGuildConfig, DiscordIntentsConfig,
DiscordThreadBindingsConfig, DiscordUiComponentsConfig, DiscordUiConfig,
DiscordVoiceAutoJoinConfig, DiscordVoiceConfig, EndpointProvidersConfig, EnvironmentConfig,
EnvironmentVariableOverride, ExperimentalFeatures, FeishuAccountConfig, FeishuBotConfig,
FeishuGroupConfig, GatewayConfig, GatewayTunnelConfig, GeminiApiKeyEntry,
HintRouteSettingsEntry, HintRouterSettings, ImageGenConfig, InjectionRuleConfig,
InjectionSettings, LoggingConfig, MemoryAutoConfig, MemoryConfig, MemoryEmbeddingConfig,
MemoryEmbeddingProvider, MemoryProfileConfig, MemoryResolveConfig,
ChatAppearanceConfig, CloudflareTunnelConfig, Config, ConversationSettings,
CrashReportingConfig, CredentialEntry, CredentialPoolConfig, CustomProviderConfig,
DeliveryConfig, DiscordAccountConfig, DiscordActionsConfig, DiscordAgentComponentsConfig,
DiscordAutoPresenceConfig, DiscordBotConfig, DiscordChannelConfig, DiscordExecApprovalsConfig,
DiscordGuildConfig, DiscordIntentsConfig, DiscordThreadBindingsConfig,
DiscordUiComponentsConfig, DiscordUiConfig, DiscordVoiceAutoJoinConfig, DiscordVoiceConfig,
EndpointProvidersConfig, EnvironmentConfig, EnvironmentVariableOverride, ExperimentalFeatures,
FeishuAccountConfig, FeishuBotConfig, FeishuGroupConfig, GatewayConfig, GatewayTunnelConfig,
GeminiApiKeyEntry, HintRouteSettingsEntry, HintRouterSettings, ImageGenConfig,
InjectionRuleConfig, InjectionSettings, LoggingConfig, MemoryAutoConfig, MemoryConfig,
MemoryEmbeddingConfig, MemoryEmbeddingProvider, MemoryProfileConfig, MemoryResolveConfig,
MemorySoulArtifactVoiceConfig, MemorySoulArtifactVoiceSource, MemorySoulConfig,
MemorySoulImportSource, MemorySourcesConfig, ModelInfo, ModelsConfig, MultiSearchConfig,
MultiSearchEngineEntryConfig, NativeAgentConfig, NavigationConfig, OpenAIAsrConfig,
+7 -78
View File
@@ -692,9 +692,6 @@ pub struct WorkspacePreferencesConfig {
/// 全局媒体生成默认设置
#[serde(default)]
pub media_defaults: MediaGenerationDefaultsConfig,
/// 桌宠能力偏好设置
#[serde(default)]
pub companion_defaults: CompanionDefaultsConfig,
/// 服务模型设置
#[serde(default, skip_serializing_if = "ServiceModelsConfig::is_default")]
pub service_models: ServiceModelsConfig,
@@ -710,7 +707,6 @@ impl Default for WorkspacePreferencesConfig {
schema_version: current_workspace_preferences_schema_version(),
agent_response_language: None,
media_defaults: MediaGenerationDefaultsConfig::default(),
companion_defaults: CompanionDefaultsConfig::default(),
service_models: ServiceModelsConfig::default(),
}
}
@@ -752,14 +748,6 @@ pub struct MediaGenerationDefaultsConfig {
pub voice: MediaGenerationPreferenceConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct CompanionDefaultsConfig {
#[serde(default)]
pub general: MediaGenerationPreferenceConfig,
#[serde(default)]
pub tts: MediaGenerationPreferenceConfig,
}
fn default_service_model_enabled() -> bool {
true
}
@@ -854,13 +842,13 @@ impl ServiceModelsConfig {
/// 导航栏模块配置
///
/// 配置左侧导航栏里可显式开启的隐藏系统入口
/// 保留历史导航配置字段;当前不再提供可显式开启的隐藏系统入口
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NavigationConfig {
/// 工作区导航默认值版本
#[serde(default)]
pub schema_version: u8,
/// 已显式开启的隐藏系统入口列表;固定主导航不写入配置
/// 历史隐藏系统入口列表;归一化时清空,固定主导航不写入配置
#[serde(default = "default_enabled_nav_items")]
pub enabled_items: Vec<String>,
}
@@ -878,7 +866,7 @@ impl Default for NavigationConfig {
}
}
const CURRENT_CONFIGURABLE_NAV_ITEM_IDS: &[&str] = &["plugins", "companion"];
const CURRENT_CONFIGURABLE_NAV_ITEM_IDS: &[&str] = &[];
fn normalize_navigation_enabled_items(items: &[String]) -> Vec<String> {
let mut normalized = Vec::new();
@@ -2984,17 +2972,14 @@ mod unit_tests {
}
#[test]
fn test_normalize_workspace_preferences_preserves_current_custom_values() {
fn test_normalize_workspace_preferences_clears_current_custom_values() {
let mut config = Config::default();
config.navigation.enabled_items = vec!["plugins".to_string(), "companion".to_string()];
let changed = config.normalize_workspace_preferences();
assert!(!changed);
assert_eq!(
config.navigation.enabled_items,
vec!["plugins".to_string(), "companion".to_string()]
);
assert!(changed);
assert!(config.navigation.enabled_items.is_empty());
}
#[test]
@@ -3031,63 +3016,7 @@ mod unit_tests {
let changed = config.normalize_workspace_preferences();
assert!(changed);
assert_eq!(
config.navigation.enabled_items,
vec!["plugins".to_string(), "companion".to_string()]
);
}
#[test]
fn test_workspace_preferences_supports_companion_defaults_roundtrip() {
let mut config = Config::default();
config
.workspace_preferences
.companion_defaults
.general
.preferred_provider_id = Some("deepseek".to_string());
config
.workspace_preferences
.companion_defaults
.general
.preferred_model_id = Some("deepseek-chat".to_string());
config
.workspace_preferences
.companion_defaults
.tts
.preferred_provider_id = Some("openai-tts".to_string());
let value =
serde_json::to_value(&config).expect("config should serialize companion defaults");
let parsed: Config =
serde_json::from_value(value).expect("config should deserialize companion defaults");
assert_eq!(
parsed
.workspace_preferences
.companion_defaults
.general
.preferred_provider_id
.as_deref(),
Some("deepseek")
);
assert_eq!(
parsed
.workspace_preferences
.companion_defaults
.general
.preferred_model_id
.as_deref(),
Some("deepseek-chat")
);
assert_eq!(
parsed
.workspace_preferences
.companion_defaults
.tts
.preferred_provider_id
.as_deref(),
Some("openai-tts")
);
assert!(config.navigation.enabled_items.is_empty());
}
#[test]
+4 -10
View File
@@ -201,11 +201,7 @@ impl SkillService {
app_type: &AppType,
_installed_states: &HashMap<String, SkillState>,
) -> Result<Vec<Skill>> {
self.list_local_skills_with_scope(
app_type,
_installed_states,
LocalSkillCatalogScope::All,
)
self.list_local_skills_with_scope(app_type, _installed_states, LocalSkillCatalogScope::All)
}
pub fn list_local_skills_with_scope(
@@ -1820,11 +1816,9 @@ metadata:
#[test]
fn user_scope_catalog_roots_should_exclude_project_roots() {
let roots = SkillService::get_catalog_roots_for_scope(
&AppType::Lime,
LocalSkillCatalogScope::User,
)
.expect("resolve user skill roots");
let roots =
SkillService::get_catalog_roots_for_scope(&AppType::Lime, LocalSkillCatalogScope::User)
.expect("resolve user skill roots");
assert!(roots
.iter()
+13 -12
View File
@@ -1,12 +1,12 @@
{
"name": "lime",
"version": "1.65.0",
"version": "1.66.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lime",
"version": "1.65.0",
"version": "1.66.0",
"license": "GPL-3.0-only",
"workspaces": [
"packages/*"
@@ -14,7 +14,7 @@
"dependencies": {
"@babel/standalone": "^7.29.0",
"@floating-ui/react": "^0.27.16",
"@limecloud/agent-runtime-client": "0.1.0",
"@limecloud/agent-runtime-client": "0.1.1",
"@limecloud/agent-runtime-projection": "0.1.0",
"@limecloud/agent-runtime-ui": "0.1.0",
"@limecloud/agent-ui-contracts": "0.1.0",
@@ -3071,6 +3071,10 @@
"resolved": "packages/agent-ui-contracts",
"link": true
},
"node_modules/@limecloud/app-server-client": {
"resolved": "packages/app-server-client",
"link": true
},
"node_modules/@limecloud/lime-cli": {
"resolved": "packages/lime-cli-npm",
"link": true
@@ -7704,10 +7708,6 @@
"node": ">= 8"
}
},
"node_modules/app-server-client": {
"resolved": "packages/app-server-client",
"link": true
},
"node_modules/appdmg": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/appdmg/-/appdmg-0.6.6.tgz",
@@ -19793,7 +19793,7 @@
},
"packages/agent-app-runtime": {
"name": "@limecloud/agent-app-runtime",
"version": "1.65.0",
"version": "1.66.0",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
@@ -19801,10 +19801,10 @@
},
"packages/agent-runtime-client": {
"name": "@limecloud/agent-runtime-client",
"version": "0.1.0",
"version": "0.1.1",
"license": "MIT",
"dependencies": {
"app-server-client": "1.65.0"
"@limecloud/app-server-client": "1.66.0"
},
"engines": {
"node": ">=20.0.0"
@@ -19848,7 +19848,8 @@
}
},
"packages/app-server-client": {
"version": "1.65.0",
"name": "@limecloud/app-server-client",
"version": "1.66.0",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
@@ -19856,7 +19857,7 @@
},
"packages/lime-cli-npm": {
"name": "@limecloud/lime-cli",
"version": "1.65.0",
"version": "1.66.0",
"cpu": [
"x64",
"arm64"
+2 -2
View File
@@ -4,7 +4,7 @@
"workspaces": [
"packages/*"
],
"version": "1.65.0",
"version": "1.66.0",
"description": "AI content workspace for Chinese creators: desktop writing, research, prompt management, knowledge base, and multi-model workflows.",
"keywords": [
"ai",
@@ -223,7 +223,7 @@
"dependencies": {
"@babel/standalone": "^7.29.0",
"@floating-ui/react": "^0.27.16",
"@limecloud/agent-runtime-client": "0.1.0",
"@limecloud/agent-runtime-client": "0.1.1",
"@limecloud/agent-runtime-projection": "0.1.0",
"@limecloud/agent-runtime-ui": "0.1.0",
"@limecloud/agent-ui-contracts": "0.1.0",
+1 -1
View File
@@ -12,7 +12,7 @@ const distDir = resolve(__dirname, 'dist');
const workspaceAliases = [
{ find: '@', replacement: resolve(root, 'src') },
{
find: 'app-server-client',
find: '@limecloud/app-server-client',
replacement: resolve(root, 'packages/app-server-client/src/index.ts'),
},
{
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@limecloud/agent-app-runtime",
"version": "1.65.0",
"version": "1.66.0",
"type": "module",
"description": "Lime Agent App browser-side runtime SDK: bridge transport, capability adapters, and task orchestration.",
"license": "MIT",
+169 -3
View File
@@ -1,6 +1,6 @@
# @limecloud/agent-runtime-client
`@limecloud/agent-runtime-client` 是 Lime Agent Runtime 的标准 TypeScript client facade。它不定义新协议,也不重建运行时;当前实现只复用 `app-server-client` 中已经存在的 App Server JSON-RPC current 主链。
`@limecloud/agent-runtime-client` 是 Lime Agent Runtime 的标准 TypeScript client facade。它不定义新协议,也不重建运行时;当前实现只复用 `@limecloud/app-server-client` 中已经存在的 App Server JSON-RPC current 主链。
## Boundary
@@ -8,6 +8,7 @@
- 暴露 `AgentRuntimeClient` 标准接口。
- 暴露 `createAgentRuntimeClient(...)` 工厂。
- 暴露 browser-safe 子路径 `@limecloud/agent-runtime-client/sessionGateway`,用于把已有 App Server session gateway 适配为标准 runtime client。
- 委托 App Server current methods`agentSession/turn/start``agentSession/turn/cancel``agentSession/action/respond``agentSession/read``evidence/export`
- 订阅和分发 `agentSession/event` runtime events。
@@ -18,7 +19,31 @@
- 直接渲染 UI。
- 伪造独立 `readTask` 协议。
## Usage
## Install
```bash
npm install @limecloud/agent-runtime-client
```
根入口面向 Node / host 侧 App Server transportrenderer 或浏览器 bundle 默认使用 `./sessionGateway` 子路径,避免把 sidecar / stdio 能力打进前端包。
## Package Mapping
AgentUI / AgentRuntime 当前四包链路:
```text
@limecloud/agent-runtime-client
-> App Server JSON-RPC current facade
-> agentSession/event + agentSession/read
-> @limecloud/agent-runtime-projection
-> @limecloud/agent-runtime-ui
```
`@limecloud/agent-runtime-client` 只拥有 transport facade。它不导出 `AgentUiProjectionState`,也不依赖 `@limecloud/agent-runtime-projection``@limecloud/agent-runtime-ui`
## Root Usage
根入口适合 App Server connection owner 使用。它直接复用 `@limecloud/app-server-client``AppServerConnection`,不新增 runtime protocol
```ts
import {
@@ -46,9 +71,150 @@ const thread = await runtime.readThread({ sessionId: "session-1" });
`readThread` 当前映射到 App Server `agentSession/read`。如果宿主需要 task 视图,应先从 session、turns 和 events 投影,不要在 client 包里伪造第二套 task protocol。
## Client API
| Method | App Server owner | Result | Rule |
| --- | --- | --- | --- |
| `startTurn(params, options?)` | `agentSession/turn/start` | turn start response | 只提交 runtime intent,不生成 UI state。 |
| `cancelTurn(params, options?)` | `agentSession/turn/cancel` | cancel response | 失败直接向上抛出,不本地假设已取消。 |
| `respondAction(params, options?)` | `agentSession/action/respond` | action response | 不乐观改 pending action,等待 runtime facts。 |
| `readThread(params, options?)` | `agentSession/read` | session read model | 供 projection hydration / repair 使用。 |
| `exportEvidence(params, options?)` | `evidence/export` | evidence export response | 缺 surface 时 fail closed,不伪造空 evidence。 |
| `subscribeEvents(listener)` | `agentSession/event` | unsubscribe handle | 只分发 App Server runtime events。 |
| `dispatchEvent(message)` | local event router | boolean | 用于现有 gateway 把 JSON-RPC notification 喂给 client。 |
| `nextEvent(timeoutMs?)` | gateway event source | notification | 优先 gateway `nextEvent`,其次 `drainEvents`。 |
`options.timeoutMs` 只影响底层 App Server request / event source。超时不等于 turn 失败;宿主应再用 `readThread` 或 runtime event 修复状态。
## Runtime Lifecycle
标准 turn 生命周期由 App Server / RuntimeCore 拥有:
```text
startTurn
-> agentSession/event: turn.started / model.delta / tool.* / action.*
-> readThread for hydration or repair
-> respondAction / cancelTurn when user intent occurs
-> agentSession/event: turn.completed / turn.failed / action.resolved
-> exportEvidence when host needs replay / review package
```
client 只传递 lifecycle intent 和 facts,不维护 tool 状态机、subagent lineage、Provider 参数或 UI 完成态。
## Browser-Safe Session Gateway
Renderer 宿主如果已经有自己的 App Server gateway,应从 browser-safe 子路径导入 session gateway 适配器,避免把根入口中的 Node 侧 sidecar / stdio client 打进前端包。该适配器仍返回标准 `AgentRuntimeClient`,覆盖 turn lifecycle、`readThread``exportEvidence``subscribeEvents``dispatchEvent``nextEvent`;宿主缺少 evidence 或 event source 时会 fail closed,不会静默回退 mock。
推荐传入函数式 gateway。如果你的 `AppServerClient` 是 class instance,方法内部依赖 `this.request`,不要直接裸传类方法,应在产品网关里包成闭包:
```ts
import { createAgentRuntimeClientFromSessionGateway } from "@limecloud/agent-runtime-client/sessionGateway";
const runtime = createAgentRuntimeClientFromSessionGateway({
startTurn: (params, options) => appServerClient.startTurn(params, options),
readSession: (params, options) => appServerClient.readSession(params, options),
cancelTurn: (params, options) => appServerClient.cancelTurn(params, options),
respondAction: (params, options) => appServerClient.respondAction(params, options),
exportEvidence: (params, options) => appServerClient.exportEvidence(params, options),
nextEvent: (timeoutMs) => appServerClient.nextEvent(timeoutMs),
});
```
最小 lifecycle-only 宿主可以只实现 turn、read 和 action response
```ts
import type { AgentRuntimeLifecycleClient } from "@limecloud/agent-runtime-client/sessionGateway";
const runtime: AgentRuntimeLifecycleClient =
createAgentRuntimeClientFromSessionGateway({
startTurn: (params) => appServerClient.startTurn(params),
readSession: (params) => appServerClient.readSession(params),
cancelTurn: (params) => appServerClient.cancelTurn(params),
respondAction: (params) => appServerClient.respondAction(params),
});
```
## Transport Contract
| Transport surface | Current rule |
| --- | --- |
| Root entry | 适合 Node / host owner,复用 `@limecloud/app-server-client``AppServerConnection`。 |
| `./sessionGateway` | browser-safe,适合 renderer 复用已有 App Server gateway。 |
| Electron / Desktop Host | 只能由宿主 gateway 封装,不能在本包直接 import bridge helper。 |
| Event source | `nextEvent(timeoutMs?)``drainEvents(limit?)`,只接受 `agentSession/event` notification。 |
| Mock / fixture | 只允许测试显式传入,不允许 production transport fallback。 |
如果 gateway 方法来自 class instance,必须在宿主里包成闭包,避免丢失 `this`。本包不会替宿主绑定私有 client 实例。
## Product App Flow
产品应用接入时保持这条链路:
```text
Product App business context
-> AgentRuntimeClient.startTurn / readThread / respondAction
-> App Server agentSession/*
-> RuntimeCore / ExecutionBackend
-> agentSession/event + agentSession/read
-> @limecloud/agent-runtime-projection
-> @limecloud/agent-runtime-ui
```
产品应用只负责 session 归属、workspace、业务对象 id、action callback 和页面路由;不要在产品应用里重建 turn 状态机、tool 状态机、Provider 参数拼装或 mock fallback。
## Error Handling
这个包不吞掉 transport error。调用方应按真实边界处理:
- bridge / network errorfail closed,提示运行时不可用。
- App Server error:保留原始 request result 或抛错,不转换成 UI 成功态。
- `exportEvidence` 未实现:按宿主能力缺失处理,不伪造空 evidence。
- event source 未实现:`nextEvent` 抛错,宿主可改用 read model hydration。
- action response 失败:保留 pending action,由宿主提示重试。
错误分类建议:
| Failure | UI expectation |
| --- | --- |
| bridge unavailable | runtime blocked / unavailable。 |
| provider not ready | settings / setup action,由 App Server facts 表达。 |
| stream interrupted | hydration `stale` / `repairing`,由 read model 修复。 |
| action response failed | action 仍 pending,可重试。 |
| evidence export missing | evidence panel 显示能力缺失,不伪造导出包。 |
## Conformance
最小 runtime client conformance
- lifecycle`startTurn -> readThread -> cancelTurn / respondAction` 均委托 gateway。
- events`subscribeEvents``dispatchEvent``nextEvent` 只处理 `agentSession/event`
- evidence`exportEvidence` 有实现时委托,没有实现时 fail closed。
- errorstransport error 原样传播,不切 mock。
- bundle`@limecloud/agent-runtime-client/sessionGateway` dist 不包含 Node builtin 或 `@limecloud/app-server-client` 依赖。
## Development
```bash
npm --prefix packages/app-server-client run build
npm --prefix packages/agent-runtime-client run build
npm --prefix packages/agent-runtime-client run test
npm run test:contracts
```
## Package Metadata
| Item | Value |
| --- | --- |
| Runtime | Node `>=20`ESM。 |
| Root dependency | `@limecloud/app-server-client`。 |
| Browser-safe export | `@limecloud/agent-runtime-client/sessionGateway`。 |
| Side effects | `false`。 |
| Public files | `dist``README.md`。 |
| License | `MIT`。 |
## Do Not
- 不要在这里新增 JSON-RPC method name。
- 不要在这里生成 `AgentUiProjectionState`
- 不要在这里导入 React、Electron bridge、renderer bridge helper 或测试 mock registry。
- 不要把 Provider API key、Provider SDK client 或 direct HTTP 放进 runtime client。
- 不要把旧 Agent App desktop facade 当成新 runtime owner。
+6 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@limecloud/agent-runtime-client",
"version": "0.1.0",
"version": "0.1.1",
"type": "module",
"description": "Standard Agent Runtime client facade over the current App Server JSON-RPC runtime surface.",
"license": "MIT",
@@ -25,6 +25,10 @@
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./sessionGateway": {
"types": "./dist/sessionGateway.d.ts",
"import": "./dist/sessionGateway.js"
}
},
"files": [
@@ -38,7 +42,7 @@
"test": "npm run build && node --test tests/*.test.mjs"
},
"dependencies": {
"app-server-client": "1.65.0"
"@limecloud/app-server-client": "1.66.0"
},
"sideEffects": false
}
+7 -1
View File
@@ -21,4 +21,10 @@ export {
type AppServerRequestResult,
type EvidenceExportParams,
type EvidenceExportResponse,
} from "app-server-client";
} from "@limecloud/app-server-client";
export {
createAgentRuntimeClientFromSessionGateway,
type AgentRuntimeLifecycleClient,
type AgentRuntimeSessionGateway,
} from "./sessionGateway.js";
@@ -0,0 +1,200 @@
import type {
AgentRuntimeClient,
AgentRuntimeClientSubscription,
AgentRuntimeEventListener,
AgentSessionActionRespondParams,
AgentSessionActionRespondResponse,
AgentSessionEventNotification,
AgentSessionReadParams,
AgentSessionReadResponse,
AgentSessionTurnCancelParams,
AgentSessionTurnCancelResponse,
AgentSessionTurnStartParams,
AgentSessionTurnStartResponse,
AppServerRequestOptions,
AppServerRequestResult,
EvidenceExportParams,
EvidenceExportResponse,
JsonRpcMessage,
JsonRpcNotification,
} from "@limecloud/app-server-client";
const METHOD_AGENT_SESSION_EVENT = "agentSession/event";
export type AgentRuntimeLifecycleClient = Pick<
AgentRuntimeClient,
"startTurn" | "readThread" | "cancelTurn" | "respondAction"
>;
type AgentRuntimeGatewayMethod<Params, Result> = (
params: Params,
options?: AppServerRequestOptions,
) => Promise<AppServerRequestResult<Result>>;
export type AgentRuntimeSessionGateway = {
startTurn: AgentRuntimeGatewayMethod<
AgentSessionTurnStartParams,
AgentSessionTurnStartResponse
>;
readSession: AgentRuntimeGatewayMethod<
AgentSessionReadParams,
AgentSessionReadResponse
>;
cancelTurn: AgentRuntimeGatewayMethod<
AgentSessionTurnCancelParams,
AgentSessionTurnCancelResponse
>;
respondAction: AgentRuntimeGatewayMethod<
AgentSessionActionRespondParams,
AgentSessionActionRespondResponse
>;
exportEvidence?: AgentRuntimeGatewayMethod<
EvidenceExportParams,
EvidenceExportResponse
>;
nextEvent?(timeoutMs?: number): Promise<AgentSessionEventNotification>;
drainEvents?(limit?: number): Promise<JsonRpcMessage[]>;
};
export function createAgentRuntimeClientFromSessionGateway(
gateway: AgentRuntimeSessionGateway,
): AgentRuntimeClient {
const eventRouter = new AgentRuntimeGatewayEventRouter();
return {
startTurn: (params, options) =>
callAgentRuntimeSessionGateway(gateway.startTurn, params, options),
readThread: (params, options) =>
callAgentRuntimeSessionGateway(gateway.readSession, params, options),
cancelTurn: (params, options) =>
callAgentRuntimeSessionGateway(gateway.cancelTurn, params, options),
respondAction: (params, options) =>
callAgentRuntimeSessionGateway(gateway.respondAction, params, options),
exportEvidence: (params, options) =>
callOptionalAgentRuntimeSessionGateway(
gateway.exportEvidence,
"exportEvidence",
params,
options,
),
subscribeEvents(listener) {
return eventRouter.subscribe(listener);
},
async dispatchEvent(message) {
return await eventRouter.dispatch(message);
},
async nextEvent(timeoutMs) {
if (gateway.nextEvent) {
const notification = await gateway.nextEvent(timeoutMs);
await eventRouter.dispatch(notification);
return notification;
}
if (gateway.drainEvents) {
return await nextDrainedAgentRuntimeEvent(
gateway,
eventRouter,
timeoutMs,
);
}
throw new Error(
"AgentRuntime session gateway does not expose agentSession/event subscription.",
);
},
};
}
function callAgentRuntimeSessionGateway<Params, Result>(
method: (
params: Params,
options?: AppServerRequestOptions,
) => Promise<AppServerRequestResult<Result>>,
params: Params,
options?: AppServerRequestOptions,
): Promise<AppServerRequestResult<Result>> {
if (options === undefined) {
return method(params);
}
return method(params, options);
}
async function callOptionalAgentRuntimeSessionGateway<Params, Result>(
method: AgentRuntimeGatewayMethod<Params, Result> | undefined,
methodName: string,
params: Params,
options?: AppServerRequestOptions,
): Promise<AppServerRequestResult<Result>> {
if (!method) {
throw new Error(
`AgentRuntime session gateway does not expose ${methodName}.`,
);
}
return callAgentRuntimeSessionGateway(method, params, options);
}
async function nextDrainedAgentRuntimeEvent(
gateway: AgentRuntimeSessionGateway,
eventRouter: AgentRuntimeGatewayEventRouter,
timeoutMs?: number,
): Promise<AgentSessionEventNotification> {
const messages = await gateway.drainEvents?.(1);
for (const message of messages ?? []) {
const notification = agentSessionEventNotificationFromMessage(message);
if (notification) {
await eventRouter.dispatch(notification);
return notification;
}
}
throw new Error(
timeoutMs === undefined
? "AgentRuntime session gateway drainEvents did not return agentSession/event."
: `AgentRuntime session gateway drainEvents did not return agentSession/event within ${timeoutMs}ms.`,
);
}
class AgentRuntimeGatewayEventRouter {
readonly #listeners = new Set<AgentRuntimeEventListener>();
subscribe(listener: AgentRuntimeEventListener): AgentRuntimeClientSubscription {
this.#listeners.add(listener);
return {
unsubscribe: () => {
this.#listeners.delete(listener);
},
};
}
async dispatch(message: JsonRpcMessage): Promise<boolean> {
const notification = agentSessionEventNotificationFromMessage(message);
if (!notification) {
return false;
}
for (const listener of this.#listeners) {
await listener(notification.params.event, notification);
}
return true;
}
}
function agentSessionEventNotificationFromMessage(
message: JsonRpcMessage,
): AgentSessionEventNotification | undefined {
if (!isJsonRpcNotification(message)) {
return undefined;
}
if (message.method !== METHOD_AGENT_SESSION_EVENT) {
return undefined;
}
if (!isRecord(message.params) || !("event" in message.params)) {
return undefined;
}
return message as AgentSessionEventNotification;
}
function isJsonRpcNotification(
message: JsonRpcMessage,
): message is JsonRpcNotification {
return isRecord(message) && "method" in message && !("id" in message);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
@@ -1,9 +1,11 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import {
AppServerConnection,
createAgentRuntimeClient,
createAgentRuntimeClientFromSessionGateway,
} from "../dist/index.js";
test("createAgentRuntimeClient is available from the standard runtime client package", async () => {
@@ -36,3 +38,299 @@ test("createAgentRuntimeClient is available from the standard runtime client pac
assert.equal(sent[0].method, "agentSession/turn/start");
assert.equal(result.result.turn.turnId, "turn-1");
});
test("createAgentRuntimeClientFromSessionGateway adapts an existing session gateway", async () => {
const calls = [];
const gateway = {
async startTurn(params, options) {
calls.push(["startTurn", params, options]);
return {
id: 1,
result: {
turn: {
turnId: "turn-1",
sessionId: params.sessionId,
threadId: "thread-1",
status: "accepted",
},
},
response: { jsonrpc: "2.0", id: 1, result: {} },
notifications: [],
messages: [],
};
},
async readSession(params, options) {
calls.push(["readSession", params, options]);
return {
id: 2,
result: {
session: {
sessionId: params.sessionId,
threadId: "thread-1",
status: "running",
createdAt: "2026-05-15T00:00:00.000Z",
updatedAt: "2026-05-15T00:00:01.000Z",
},
turns: [],
},
response: { jsonrpc: "2.0", id: 2, result: {} },
notifications: [],
messages: [],
};
},
async cancelTurn(params, options) {
calls.push(["cancelTurn", params, options]);
return {
id: 3,
result: {},
response: { jsonrpc: "2.0", id: 3, result: {} },
notifications: [],
messages: [],
};
},
async respondAction(params, options) {
calls.push(["respondAction", params, options]);
return {
id: 4,
result: {},
response: { jsonrpc: "2.0", id: 4, result: {} },
notifications: [],
messages: [],
};
},
async exportEvidence(params, options) {
calls.push(["exportEvidence", params, options]);
return {
id: 5,
result: {
session: {
sessionId: params.sessionId,
threadId: "thread-1",
status: "completed",
createdAt: "2026-05-15T00:00:00.000Z",
updatedAt: "2026-05-15T00:00:02.000Z",
},
turns: [],
events: [],
artifacts: [],
exportedAt: "2026-05-15T00:00:03.000Z",
},
response: { jsonrpc: "2.0", id: 5, result: {} },
notifications: [],
messages: [],
};
},
};
const runtime = createAgentRuntimeClientFromSessionGateway(gateway);
const requestOptions = { timeoutMs: 120_000 };
await runtime.startTurn(
{ sessionId: "session-1", input: { text: "生成草稿" } },
requestOptions,
);
await runtime.readThread({ sessionId: "session-1" }, requestOptions);
await runtime.cancelTurn(
{ sessionId: "session-1", turnId: "turn-1" },
requestOptions,
);
await runtime.respondAction(
{
sessionId: "session-1",
requestId: "request-1",
actionType: "ask_user",
confirmed: true,
response: "继续",
},
requestOptions,
);
await runtime.exportEvidence(
{
sessionId: "session-1",
includeEvents: true,
includeArtifacts: true,
},
requestOptions,
);
assert.deepEqual(calls.map(([name]) => name), [
"startTurn",
"readSession",
"cancelTurn",
"respondAction",
"exportEvidence",
]);
assert.equal(calls[1][1].sessionId, "session-1");
assert.equal(calls[1][2].timeoutMs, 120_000);
assert.equal(calls[4][1].includeEvents, true);
});
test("session gateway client dispatches only App Server runtime events", async () => {
const runtime = createAgentRuntimeClientFromSessionGateway(
createMinimalSessionGateway(),
);
const received = [];
const subscription = runtime.subscribeEvents((event, notification) => {
received.push([event.type, notification.method]);
});
const ignored = await runtime.dispatchEvent({
method: "log/list",
params: {},
});
const handled = await runtime.dispatchEvent(
agentSessionEventNotification("turn.started"),
);
subscription.unsubscribe();
await runtime.dispatchEvent(agentSessionEventNotification("turn.completed"));
assert.equal(ignored, false);
assert.equal(handled, true);
assert.deepEqual(received, [["turn.started", "agentSession/event"]]);
});
test("session gateway client reads nextEvent from gateway event sources", async () => {
const directRuntime = createAgentRuntimeClientFromSessionGateway({
...createMinimalSessionGateway(),
async nextEvent(timeoutMs) {
assert.equal(timeoutMs, 500);
return agentSessionEventNotification("tool.started");
},
});
const directReceived = [];
directRuntime.subscribeEvents((event) => {
directReceived.push(event.type);
});
const directEvent = await directRuntime.nextEvent(500);
assert.equal(directEvent.params.event.type, "tool.started");
assert.deepEqual(directReceived, ["tool.started"]);
const drainedRuntime = createAgentRuntimeClientFromSessionGateway({
...createMinimalSessionGateway(),
async drainEvents(limit) {
assert.equal(limit, 1);
return [
{ method: "log/list", params: {} },
agentSessionEventNotification("action.required"),
];
},
});
const drainedEvent = await drainedRuntime.nextEvent();
assert.equal(drainedEvent.params.event.type, "action.required");
});
test("session gateway client fails closed when required runtime surfaces are missing", async () => {
const runtime = createAgentRuntimeClientFromSessionGateway(
createMinimalSessionGateway(),
);
await assert.rejects(
runtime.exportEvidence({ sessionId: "session-1" }),
/does not expose exportEvidence/,
);
await assert.rejects(runtime.nextEvent(), /does not expose agentSession\/event/);
});
test("session gateway client propagates transport errors without fallback mocks", async () => {
const runtime = createAgentRuntimeClientFromSessionGateway({
...createMinimalSessionGateway(),
async startTurn() {
throw new Error("bridge offline");
},
});
await assert.rejects(
runtime.startTurn({ sessionId: "session-1", input: { text: "生成草稿" } }),
/bridge offline/,
);
});
test("sessionGateway subpath remains browser-safe", async () => {
const source = await readFile(
new URL("../dist/sessionGateway.js", import.meta.url),
"utf8",
);
assert.equal(source.includes("node:"), false);
assert.equal(source.includes("app-server-client"), false);
assert.equal(source.includes("@limecloud/app-server-client"), false);
});
function createMinimalSessionGateway() {
return {
async startTurn(params) {
return {
id: 1,
result: {
turn: {
turnId: "turn-1",
sessionId: params.sessionId,
threadId: "thread-1",
status: "accepted",
},
},
response: { jsonrpc: "2.0", id: 1, result: {} },
notifications: [],
messages: [],
};
},
async readSession(params) {
return {
id: 2,
result: {
session: {
sessionId: params.sessionId,
threadId: "thread-1",
status: "running",
createdAt: "2026-05-15T00:00:00.000Z",
updatedAt: "2026-05-15T00:00:01.000Z",
},
turns: [],
},
response: { jsonrpc: "2.0", id: 2, result: {} },
notifications: [],
messages: [],
};
},
async cancelTurn() {
return {
id: 3,
result: {},
response: { jsonrpc: "2.0", id: 3, result: {} },
notifications: [],
messages: [],
};
},
async respondAction() {
return {
id: 4,
result: {},
response: { jsonrpc: "2.0", id: 4, result: {} },
notifications: [],
messages: [],
};
},
};
}
function agentSessionEventNotification(type) {
return {
method: "agentSession/event",
params: {
event: {
eventId: `event-${type}`,
sequence: 1,
sessionId: "session-1",
threadId: "thread-1",
turnId: "turn-1",
type,
timestamp: "2026-05-15T00:00:02.000Z",
payload: {},
},
},
};
}
+198 -4
View File
@@ -4,6 +4,8 @@
这个包只包含纯 TypeScript 类型和函数,不依赖 React,不调用 JSON-RPC,不访问 Electron IPC,也不绑定任何具体业务模块。
当前物理包名是 `@limecloud/agent-runtime-projection`。历史规划中出现过的 `@limecloud/agent-ui-projection` 只能作为未来 alias 或文档历史引用,不能写成 current owner。
## Standard Package Layering
Agent UI / Runtime 的标准链路由四个包共同组成:
@@ -23,7 +25,7 @@ Agent UI / Runtime 的标准链路由四个包共同组成:
- `@limecloud/agent-runtime-projection` 只做 headless projection 和 selectors,不接触宿主、网络、React 或业务对象。
- `@limecloud/agent-runtime-ui` 只消费 projection state 渲染标准 React primitives,不订阅运行时。
宿主负责把 App Server 事件适配为标准 execution events,再按 `AgentRuntimeClient -> executionEvents -> projectAgentUiState -> AgentUiProjectionView` 或自己的 presentation adapter 渲染。宿主可以保留本地化、点击路由、业务卡片和页面布局;不得重新实现 scope 匹配、latest index、summary selector 或运行时事实解释。
宿主负责把 App Server 事件适配为标准 execution events,再按 `AgentRuntimeClient -> projectAgentUiState -> AgentUiProjectionView` 或自己的 presentation adapter 渲染;如果需要展开细节,中间事件链路是 `AgentRuntimeClient -> executionEvents -> projectAgentUiState -> AgentUiProjectionView`。宿主可以保留本地化、点击路由、业务卡片和页面布局;不得重新实现 scope 匹配、latest index、summary selector 或运行时事实解释。
## Source Layout
@@ -31,6 +33,7 @@ Agent UI / Runtime 的标准链路由四个包共同组成:
```text
src/actions.ts -> HITL action.required / action.resolved projection helpers
src/appServerFacts.ts -> App Server agentSession/read、agentSession/event、evidence/export facts -> execution events replay
src/artifactEvents.ts -> artifact_snapshot projection helpers
src/contextEvents.ts -> context_trace / turn_context projection helpers
src/conversationEvents.ts -> message / text delta / reasoning delta projection helpers
@@ -48,6 +51,7 @@ src/refs.ts -> artifact refs 等跨宿主引用提取
src/routing.ts -> routing run.status / decision payload projection helpers
src/runtimeFacts.ts -> runtime entity/status/phase/topology 标准事实解释
src/summary.ts -> summary / surface / lane selectors
src/subagents.ts -> Subagents 标准模型 selector
src/subagentStatusEvents.ts -> subagent_status_changed team / handoff / worker notification projection helpers
src/threadItems.ts -> thread item -> AgentUiProjectionEvent builders / action item builders / subagent activity builders / task owner metadata parser
src/toolEvents.ts -> tool_start / tool_end / tool_progress / tool delta -> AgentUiProjectionEvent builders
@@ -72,8 +76,10 @@ npm install @limecloud/agent-ui-contracts @limecloud/agent-runtime-projection
- 用 `action.resolved` 标记已处理 action。
- 聚合 artifact refs、evidence refs、task refs、source count。
- 给 UI 提供 `UIMessageParts``ProcessTimeline``ExecutionGraph``actions``tools``readModel` 等标准渲染对象。
- 给 UI 提供 `subagents` 标准模型,统一表达子代理线程、委派调用、活动摘要和隔离摘要,避免 React 组件重新解释子代理事实。
- 为 `AgentUiProjectionEvent` 提供 host-neutral 的索引、scope selector 和 latest selector,宿主 store 只负责持久化和订阅。
- 为 `AgentUiProjectionEvent` 提供 host-neutral 的 summary selector,包括 action / task / artifact / evidence / diagnostics 计数、notable latest events、Team Workbench surface / lane 聚合和 artifact latest lookup。
- 为 `AgentUiProjectionEvent` 提供 host-neutral 的 summary selector,包括 action / task / artifact / evidence / diagnostics 计数、notable latest events、Subagents surface 聚合和 artifact latest lookup。
- 为 App Server `agentSession/read``agentSession/event``evidence/export` facts 提供标准 execution events replay adapter。
- 为宿主事件 adapter 提供 host-neutral 的 action event builder、artifact snapshot builder、context trace / turn context builder、conversation event builder、diagnostic / cost metric builder、historical hydration builder、plan approval metadata parser / builder、runtime permission builder、queue event builder、routing status builder、runtime lifecycle / runtime team / model routing / task profile builder、subagent status changed builder、thread item builder、thread item action builder、subagent activity / worker notification builder、tool lifecycle builder、TaskUpdate owner metadata parser、event base 字段、sequence 编排、字段规整、artifact refs 提取、routing decision payload、runtime entity/status/phase/topology 和 worker usage 解释函数。
这个包不负责:
@@ -86,6 +92,17 @@ npm install @limecloud/agent-ui-contracts @limecloud/agent-runtime-projection
## Usage
优先按输入来源选择入口:
| 输入来源 | 推荐入口 | 说明 |
| --- | --- | --- |
| 已有 normalized `AgentRuntimeExecutionEvent[]` | `projectAgentUiState` | 新 UI 默认入口,输出 message / timeline / graph / action / read model。 |
| 只需要旧事实栏 | `projectAgentRuntimeReadModel` | 兼容入口,不适合作为新 surface 的唯一状态。 |
| 既有产品已有 transcript messages + read model | `projectAgentUiStateFromSessionSnapshot` | 迁移期入口,把旧消息转成 `UIMessageParts`,同时复用 read model source events 生成 timeline / graph / refs / actions。 |
| App Server `agentSession/read` + `agentSession/event` + `evidence/export` facts | `replayAppServerFacts` | 不创建 client,只把已取得 facts replay 成标准 projection。 |
| contracts fixture | `replayAgentUiFixture` | 下游包和产品接入测试使用。 |
| 宿主私有事件 shape | `buildAgentUi*Event` helpers | 在 adapter 边界转成标准 projection event。 |
```ts
import {
projectAgentUiState,
@@ -124,6 +141,57 @@ const uiState = projectAgentUiState({
console.log(uiState.messages, uiState.timeline, uiState.graph);
```
最小产品接入通常长这样:
```ts
import {
projectAgentUiState,
replayAppServerFacts,
} from "@limecloud/agent-runtime-projection";
const replay = replayAppServerFacts({
readModel: await runtimeClient.readThread({ sessionId }),
events: drainedAgentSessionEvents,
evidenceExport: exportedEvidence,
});
const state = projectAgentUiState({
executionEvents: replay.events,
sourceCount: replay.state.readModel.sourceCount,
});
```
已有产品如果已经把消息 transcript 和 runtime read model 分开维护,不要在页面里继续拼 `AgentTimeline + RuntimeFactsPanel` 两套 surface。先在 adapter 边界合成标准 state,再交给 `AgentUiProjectionView`
```ts
import {
projectAgentUiStateFromSessionSnapshot,
} from "@limecloud/agent-runtime-projection";
const state = projectAgentUiStateFromSessionSnapshot({
messages: session.messages.map((message) => ({
id: message.id,
role: message.role,
content: message.content,
createdAt: message.createdAt,
})),
readModel: projectAgentRuntimeReadModel({
executionEvents: session.executionEvents,
sourceCount: session.sourceCount,
}),
});
```
如果宿主已经维护自己的 event store,应把 store 中的可见事件交给 selector,而不是在 UI 组件里重新筛选:
```ts
const visibleEvents = selectAgentUiProjectionEventsForScopeFromStore(store, {
sessionId,
turnId,
});
const summary = summarizeAgentUiProjectionEvents(visibleEvents);
```
## Event Shape
`AgentRuntimeExecutionEvent` 是跨应用的最小公共事件形状。业务应用可以使用自己的 session 类型,只要 execution event 字段兼容即可:
@@ -157,11 +225,64 @@ interface AppSession {
- `graph`: 面向任务、run、step、tool 和 action 的 `ExecutionGraph`
- `tools`: 工具事件组。
- `actions`: 待用户处理的 action。
- `subagents`: 面向 subagent / handoff / review 的标准模型,包括 `threads``delegationCalls``activities``activeThreadIds``completedThreadIds``failedThreadIds` 和轻量 `isolation` 摘要。
- `readModel`: 兼容既有事实栏的 read model。
- `runtime`: 当前运行状态。
- `hydration`: 宿主应用判断快照 / 实时流状态的水位信息。
`projectAgentRuntimeReadModel` 仍然保留,用于只需要事实栏或旧组件的宿主;新 UI 默认使用 `projectAgentUiState`
`projectAgentRuntimeReadModel` 仍然保留,用于只需要事实栏或旧组件的宿主;新 UI 默认使用 `projectAgentUiState`如果宿主正在从旧 transcript UI 迁移,使用 `projectAgentUiStateFromSessionSnapshot` 作为过渡入口;它只接受消息快照和 read model,不读取 Provider、不生成 runtime truth,也不替代 App Server facts。
`AgentUiProjectionState` 输出字段的使用边界:
- `messages`: 交给 `UIMessagePartsView` 或宿主消息流。
- `timeline`: 交给 `ProcessTimelineView`,表达工具、action、artifact、diagnostic 的线性过程。
- `graph`: 交给 `ExecutionGraphView`,表达 run / turn / task / tool / subagent 的结构关系。
- `actions`: 交给 `ActionRequiredList`,只表达可响应的人类操作。
- `subagents`: 交给 `SubagentsView` 或宿主自己的子代理 presentation adapter;业务组件不能从 assistant 正文或局部 UI state 推断 subagent / handoff / review 状态。
- `runtime`: 顶部或侧栏运行状态,不作为 Provider 状态事实源。
- `hydration`: 宿主判断 snapshot / realtime stream 是否 stale,不用于业务结果判断。
## Projection Lifecycle
Projection 的生命周期固定为 headless state machine,不绑定 React
```text
hydrate(read model / fixture / App Server facts)
-> apply runtime events by id + sequence
-> derive messages / timeline / graph / actions / tools
-> derive subagents from runtime events
-> expose hydration status
```
标准行为:
| Step | Rule |
| --- | --- |
| Hydrate | `hydrate(input)` 用新的 event 集合重建 state,不复用 React local state。 |
| Apply | `apply(event)` 按 event id 幂等追加,重复事件不改变 state。 |
| Sequence | projection 按输入顺序保留 timelinefixture validation 负责发现非法 sequence gap。 |
| Stream merge | `model.delta` / `reasoning.*` 合并为稳定 `UIMessagePart`final text 不重复追加。 |
| Snapshot repair | snapshot / read model 只能修复可见状态,不能让 UI 猜测 tool/action/artifact 终态。 |
| Subagents model | `subagents` 由 projection 构建;React 和产品应用不能再私有过滤 graph/read model。 |
| Reset | `reset()` 清空 events、read model 和 ephemeral UI。 |
当前 `hydration.status` 最小支持 `idle``live``stale``repairing``degraded` 是 contracts 预留的标准状态,必须由 runtime/read model repair 事实驱动,不能由 UI timeout 猜测。
## Selectors And State API
推荐按用途选择 API
| API | 用途 |
| --- | --- |
| `projectAgentUiState` | 一次性把 events 投影成完整 UI state。 |
| `createAgentUiProjector` | 持有 headless projector,适合 event stream apply。 |
| `projectAgentRuntimeReadModel` | 兼容事实栏或旧 surface。 |
| `buildAgentUiSubagentsModel` | 从 execution events 构建标准 Subagents 模型。 |
| `indexAgentUiProjectionEvents` | 为宿主外部 event store 建 latest / scope index。 |
| `selectAgentUiProjectionEventsForScopeFromStore` | 按 session/thread/run/turn/task scope 取可见事件。 |
| `summarizeAgentUiProjectionEvents` | 生成 host-neutral summary,不进 React。 |
这些 API 都是纯函数或纯 TypeScript 对象,不会发起 JSON-RPC,不会访问 Provider,也不会读写业务 store。
## Event Store Selectors
@@ -190,7 +311,7 @@ const visible = selectAgentUiProjectionEventsForScopeFromStore(state, {
const summary = summarizeAgentUiProjectionEvents(visible);
```
不要在业务宿主里重新实现 `session / thread / run / turn / task` scope 匹配、latest event 索引、artifact / evidence / action lookup、事件类型分组、Team Workbench surface / lane 聚合或 notable event summary。否则不同 App 会逐步长出不同的 Agent UI 解释口径。
不要在业务宿主里重新实现 `session / thread / run / turn / task` scope 匹配、latest event 索引、artifact / evidence / action lookup、事件类型分组、Subagents 聚合或 notable event summary。否则不同 App 会逐步长出不同的 Agent UI 解释口径。
也不要在业务宿主里重新实现 `buildAgentUiActionRequiredEvent``buildAgentUiActionResolvedEvent``buildAgentUiArtifactSnapshotEvent``buildAgentUiContextTraceEvent``buildAgentUiTurnContextEvents``buildAgentUiMessageSnapshotEvent``buildAgentUiTextDeltaEvent``buildAgentUiReasoningDeltaEvent``buildAgentUiHistoricalHydrationEvents``buildAgentUiPlanApprovalRequiredEvent``buildAgentUiPlanApprovalResolvedEvent``extractAgentUiPlanApprovalProjection``extractAgentUiPlanApprovalResponseProjection``buildAgentUiRuntimePermissionChangedEvent``buildAgentUiQueueAddedEvents``buildAgentUiQueueLifecycleEvents``buildAgentUiRoutingStatusEvent``buildAgentUiThreadStartedEvent``buildAgentUiRunStartedEvent``buildAgentUiRunFinishedEvent``buildAgentUiRunFailedEvent``buildAgentUiRuntimeStatusEvent``buildAgentUiRuntimeTeamChangedEvent``buildAgentUiModelChangeEvent``buildAgentUiTaskProfileResolvedEvent``buildAgentUiSubagentStatusChangedEvents``buildAgentUiThreadItemEvent``buildAgentUiThreadItemActionEvent``buildAgentUiThreadItemSubagentActivityEvent``buildAgentUiThreadItemSubagentWorkerNotificationEvent``buildAgentUiThreadItemBase``buildAgentUiToolStartEvents``buildAgentUiToolEndEvent``buildAgentUiToolEndEvents``buildAgentUiToolProgressEvent``buildAgentUiToolOutputDeltaEvent``buildAgentUiToolInputDeltaEvent``extractAgentUiTaskOwnerChangeProjection``buildAgentUiProjectionBase``sequenceAgentUiProjectionEvents``definedString``truncateText``metadataKeys``extractArtifactRefs``buildRoutingDecisionPayload``buildWorkerUsageProjection`、runtime entity/status/phase/topology 这类通用事实解释。宿主 adapter 只把自己的事件 shape 映射到标准 projection event,通用规整与事实解释必须回到本包。
@@ -210,10 +331,75 @@ App Server JSON-RPC
不要让 UI 组件直接调用 JSON-RPC。这样主聊天、工作台和后续独立 App 可以共享同一套投影逻辑,同时保留自己的后端连接方式。
如果宿主已经拿到 App Server current facts,可以直接用纯 adapter 进入标准 projection
```ts
import { replayAppServerFacts } from "@limecloud/agent-runtime-projection";
const result = replayAppServerFacts({
readModel: sessionReadResult,
events: drainedAgentSessionEvents,
evidenceExport: evidenceExportResult,
});
console.log(result.state.messages, result.state.timeline, result.state.graph);
```
`replayAppServerFacts` 只消费已取得的结构化 facts,不创建 `AppServerClient`,不订阅 JSON-RPC,也不直读数据库。`waitingAction` 会被表达为 `action.required` + waiting runtime stateartifact 和 evidence export 只通过 refs 暴露大输出,避免 UI 从 assistant 正文或 provider 原始响应猜测事实状态。
`replayAppServerFacts` 的输入建议:
- `readModel`: `AgentRuntimeClient.readThread(...)` 或 App Server `agentSession/read` 的 result。
- `events`: App Server `agentSession/event` notification 中的 `params.event` 列表。
- `evidenceExport`: App Server `evidence/export` 的 result,只提供 evidence pack refs 和 artifact refs。
不要把 JSON-RPC envelope、provider stream chunk 或 assistant Markdown 原文直接传给 projection;这些应该先在 runtime / host adapter 边界规整成 facts。
宿主应用如果把 App Server 的 `artifact.snapshot` 物化成本地业务产物,例如 Prompt 草稿、文章版本或素材交付物,应只把最终业务产物对应的 `artifact.changed` 传给投影层。上游快照可以保留在 provider 原始事件或诊断 payload 中,不要再作为第二个可见 artifact 事件传入,否则 UI 会把“中间快照”和“最终业务产物”渲染成两个并列产物。
共享投影层不会按 `artifactId``turnId` 自动合并多条 artifact 事件,因为多产物会话是合法场景。是否折叠、隐藏中间快照或提升最终产物,由宿主的业务适配层决定。
## Fixture Replay
下游包和产品接入测试应复用 contracts fixtures
```ts
import { getAgentUiFixture } from "@limecloud/agent-ui-contracts";
import { replayAgentUiFixture } from "@limecloud/agent-runtime-projection";
const fixture = getAgentUiFixture("subagent-handoff");
const replay = replayAgentUiFixture(fixture);
expect(replay.state.graph.length).toBeGreaterThan(0);
expect(replay.state.subagents.threads.length).toBeGreaterThan(0);
expect(replay.state.subagents.delegationCalls.length).toBeGreaterThan(0);
```
fixture replay 的目的不是模拟后端,而是证明同一份标准 facts 在 projection 和 UI 层得到一致解释。
## Host Adapter Pattern
宿主私有事件应只在 adapter 边界被翻译一次:
```ts
import { buildAgentUiToolStartEvents } from "@limecloud/agent-runtime-projection";
export function fromHostToolStarted(event: HostToolStarted) {
return buildAgentUiToolStartEvents({
id: event.id,
sequence: event.sequence,
sessionId: event.sessionId,
threadId: event.threadId,
runId: event.turnId,
toolCallId: event.callId,
toolName: event.name,
createdAt: event.timestamp,
});
}
```
adapter 可以读取宿主字段,但不能在组件里继续解释 tool lifecycle、subagent lineage、handoff status 或 action decision。
## Development
```bash
@@ -222,6 +408,14 @@ npm --prefix packages/agent-runtime-projection run test
npm --prefix packages/agent-runtime-projection pack --dry-run
```
## Do Not
- 不要在本包里创建 `AppServerClient`、调用 renderer bridge helper、订阅 Electron IPC 或发 HTTP。
- 不要把 Provider stream chunk、API key、HTTP response、DB row 直接暴露给 UI。
- 不要在产品应用里复制 `projectAgentUiState`、scope selector、summary selector 或 `runtimeFacts` 解释逻辑。
- 不要恢复旧树形过程术语作为 projection 标准;标准输出是 `UIMessageParts``ProcessTimeline``ExecutionGraph`
- 不要把业务 artifact 折叠策略写进共享 projection;业务物化和展示优先级由宿主 adapter 决定。
## Publish
首次发布到 npmjs
@@ -0,0 +1,641 @@
import type {
AgentRuntimeExecutionEvent,
AgentRuntimeExecutionEventKind,
AgentRuntimeExecutionEventStatus,
AgentUiProjectionState,
} from "@limecloud/agent-ui-contracts";
import {
compactProjectionFields,
definedString,
readNumberField,
readRecord,
readStringArrayField,
readStringField,
} from "./normalization.js";
import { projectAgentUiState } from "./uiState.js";
export interface AppServerAgentSessionFact {
sessionId: string;
threadId: string;
appId?: string;
workspaceId?: string;
status?: string;
createdAt?: string;
updatedAt?: string;
}
export interface AppServerAgentTurnFact {
turnId: string;
sessionId?: string;
threadId?: string;
status?: string;
startedAt?: string;
completedAt?: string;
}
export interface AppServerAgentEventFact {
eventId: string;
sequence?: number;
sessionId?: string;
threadId?: string;
turnId?: string;
type: string;
timestamp?: string;
payload?: unknown;
}
export interface AppServerArtifactSummaryFact {
artifactRef?: string;
eventId?: string;
sequence?: number;
turnId?: string;
artifactId?: string;
path?: string;
title?: string;
kind?: string;
status?: string;
contentStatus?: string;
metadata?: unknown;
}
export interface AppServerEvidencePackSummaryFact {
packRelativeRoot?: string;
packAbsoluteRoot?: string;
exportedAt?: string;
threadStatus?: string;
latestTurnStatus?: string;
turnCount?: number;
itemCount?: number;
pendingRequestCount?: number;
queuedTurnCount?: number;
recentArtifactCount?: number;
knownGaps?: string[];
artifacts?: AppServerEvidencePackArtifactFact[];
}
export interface AppServerEvidencePackArtifactFact {
kind?: string;
title?: string;
relativePath?: string;
absolutePath?: string;
bytes?: number;
}
export interface AppServerSessionReadFacts {
session: AppServerAgentSessionFact;
turns?: AppServerAgentTurnFact[];
detail?: unknown;
}
export interface AppServerEvidenceExportFacts {
session: AppServerAgentSessionFact;
turns?: AppServerAgentTurnFact[];
events?: AppServerAgentEventFact[];
artifacts?: AppServerArtifactSummaryFact[];
exportedAt?: string;
evidencePack?: AppServerEvidencePackSummaryFact;
}
export interface AppServerFactsReplayInput {
readModel?: AppServerSessionReadFacts;
events?: AppServerAgentEventFact[];
evidenceExport?: AppServerEvidenceExportFacts;
sourceCount?: number;
}
export interface AppServerFactsProjectionResult {
events: AgentRuntimeExecutionEvent[];
state: AgentUiProjectionState<AgentRuntimeExecutionEvent>;
diagnostics: string[];
}
export function projectAppServerEventsToExecutionEvents(
events: readonly AppServerAgentEventFact[] | undefined,
): AgentRuntimeExecutionEvent[] {
return dedupeExecutionEvents(
(events ?? []).map(projectAppServerEventToExecutionEvent),
);
}
export function projectAppServerSessionReadToExecutionEvents(
readModel: AppServerSessionReadFacts,
): AgentRuntimeExecutionEvent[] {
const session = readModel.session;
const turns = readModel.turns ?? [];
const events: AgentRuntimeExecutionEvent[] = [];
events.push(
compactProjectionFields({
id: `appserver:session:${session.sessionId}:snapshot`,
schemaVersion: "lime-runtime-event/v0.1",
kind: "state",
status: statusFromSessionStatus(session.status),
eventClass: "snapshot.updated",
runtimeId: "app-server",
threadId: session.threadId,
title: "Session snapshot updated",
detail: session.status,
payload: compactProjectionFields({
sessionId: session.sessionId,
appId: session.appId,
workspaceId: session.workspaceId,
status: session.status,
turnCount: turns.length,
}),
createdAt:
definedString(session.updatedAt) ??
definedString(session.createdAt) ??
defaultTimestamp(),
} satisfies AgentRuntimeExecutionEvent),
);
turns.forEach((turn, index) => {
events.push(projectAppServerTurnToExecutionEvent(turn, session, index));
});
return dedupeExecutionEvents(events);
}
export function projectAppServerEvidenceExportToExecutionEvents(
exportFacts: AppServerEvidenceExportFacts,
): AgentRuntimeExecutionEvent[] {
const events = [
...projectAppServerEventsToExecutionEvents(exportFacts.events),
...projectAppServerSessionReadToExecutionEvents({
session: exportFacts.session,
turns: exportFacts.turns,
}),
...projectAppServerArtifactsToExecutionEvents(
exportFacts.artifacts,
exportFacts.session,
exportFacts.exportedAt,
),
...projectAppServerEvidencePackToExecutionEvents(
exportFacts.evidencePack,
exportFacts.session,
exportFacts.exportedAt,
),
];
return dedupeExecutionEvents(events);
}
export function replayAppServerFacts(
input: AppServerFactsReplayInput,
): AppServerFactsProjectionResult {
const diagnostics: string[] = [];
const events = [
...(input.readModel
? projectAppServerSessionReadToExecutionEvents(input.readModel)
: []),
...projectAppServerEventsToExecutionEvents(input.events),
...(input.evidenceExport
? projectAppServerEvidenceExportToExecutionEvents(input.evidenceExport)
: []),
];
const dedupedEvents = dedupeExecutionEvents(events).sort(compareEvents);
if (dedupedEvents.length === 0) {
diagnostics.push("app_server_facts_empty");
}
return {
events: dedupedEvents,
state: projectAgentUiState({
executionEvents: dedupedEvents,
sourceCount: input.sourceCount,
}),
diagnostics,
};
}
function projectAppServerEventToExecutionEvent(
event: AppServerAgentEventFact,
): AgentRuntimeExecutionEvent {
const payload = readRecord(event.payload) ?? {};
const eventClass = normalizeEventClass(event.type);
const text = readText(payload);
const actionId = readStringField(payload, [
"request_id",
"requestId",
"action_id",
"actionId",
"id",
]);
const toolCallId = readStringField(payload, [
"toolCallId",
"tool_call_id",
"toolId",
"tool_id",
"id",
]);
const artifactRefs = normalizeRefs([
...readStringArrayField(payload, ["artifactRefs", "artifact_refs"]),
readStringField(payload, ["artifactRef", "artifact_ref"]),
readStringField(payload, ["artifactId", "artifact_id"]),
]);
const evidenceRefs = normalizeRefs([
...readStringArrayField(payload, ["evidenceRefs", "evidence_refs"]),
readStringField(payload, ["evidenceRef", "evidence_ref"]),
readStringField(payload, ["evidenceId", "evidence_id"]),
]);
return compactProjectionFields({
id: `appserver:${event.eventId}`,
schemaVersion: "lime-runtime-event/v0.1",
kind: kindForEventClass(eventClass),
status: statusForEventClass(eventClass),
eventClass,
runtimeId: "app-server",
threadId: event.threadId ?? event.sessionId,
turnId: event.turnId,
taskId: readStringField(payload, ["taskId", "task_id"]),
subagentId: readStringField(payload, ["subagentId", "subagent_id"]),
runId: readStringField(payload, ["runId", "run_id"]),
stepId: readStringField(payload, ["stepId", "step_id"]),
toolCallId: isToolEvent(eventClass) ? toolCallId : undefined,
actionId: isActionEvent(eventClass) ? actionId : undefined,
artifactId:
readStringField(payload, ["artifactId", "artifact_id"]) ??
artifactRefs[0],
evidenceId:
readStringField(payload, ["evidenceId", "evidence_id"]) ??
evidenceRefs[0],
sequence: event.sequence,
phase: phaseForEventClass(eventClass),
owner: ownerForEventClass(eventClass),
title: titleForEvent(eventClass, payload),
detail: detailForEvent(eventClass, payload, text),
refIds: normalizeRefs(readStringArrayField(payload, ["refIds", "ref_ids"])),
artifactRefs,
evidenceRefs,
payload: compactProjectionFields({
...payload,
sourceType: event.type,
sessionId: event.sessionId,
text,
messageId: readMessageId(payload),
actionKind: readStringField(payload, ["action_type", "actionType"]),
controls: controlsForAction(payload),
}),
createdAt: event.timestamp ?? defaultTimestamp(),
completedAt: isCompletedEvent(eventClass) ? event.timestamp : undefined,
} satisfies AgentRuntimeExecutionEvent);
}
function projectAppServerTurnToExecutionEvent(
turn: AppServerAgentTurnFact,
session: AppServerAgentSessionFact,
index: number,
): AgentRuntimeExecutionEvent {
const eventClass = eventClassForTurnStatus(turn.status);
return compactProjectionFields({
id: `appserver:turn:${turn.turnId}:status:${turn.status ?? "unknown"}`,
schemaVersion: "lime-runtime-event/v0.1",
kind: "state",
status: statusFromTurnStatus(turn.status),
eventClass,
runtimeId: "app-server",
threadId: turn.threadId ?? session.threadId,
turnId: turn.turnId,
sequence: index + 1,
phase: phaseForEventClass(eventClass),
title: titleForTurnStatus(turn.status),
detail: turn.status,
payload: compactProjectionFields({
sessionId: turn.sessionId ?? session.sessionId,
nativeStatus: turn.status,
}),
createdAt:
definedString(turn.completedAt) ??
definedString(turn.startedAt) ??
definedString(session.updatedAt) ??
defaultTimestamp(),
completedAt: turn.completedAt,
} satisfies AgentRuntimeExecutionEvent);
}
function projectAppServerArtifactsToExecutionEvents(
artifacts: readonly AppServerArtifactSummaryFact[] | undefined,
session: AppServerAgentSessionFact,
exportedAt: string | undefined,
): AgentRuntimeExecutionEvent[] {
return (artifacts ?? [])
.filter((artifact) => definedString(artifact.artifactRef))
.map((artifact) =>
compactProjectionFields({
id: `appserver:artifact:${artifact.artifactRef}`,
schemaVersion: "lime-runtime-event/v0.1",
kind: "draft",
owner: "artifact",
status: artifact.status === "failed" ? "failed" : "completed",
eventClass: "artifact.changed",
runtimeId: "app-server",
threadId: session.threadId,
turnId: artifact.turnId,
artifactId: artifact.artifactId ?? artifact.artifactRef,
artifactRefs: normalizeRefs([artifact.artifactRef]),
sequence: artifact.sequence,
title: artifact.title ?? "Artifact changed",
detail: artifact.path ?? artifact.kind,
payload: compactProjectionFields({
artifactRef: artifact.artifactRef,
path: artifact.path,
kind: artifact.kind,
contentStatus: artifact.contentStatus,
sourceEventId: artifact.eventId,
metadata: artifact.metadata,
}),
createdAt: exportedAt ?? defaultTimestamp(),
completedAt: exportedAt,
} satisfies AgentRuntimeExecutionEvent),
);
}
function projectAppServerEvidencePackToExecutionEvents(
evidencePack: AppServerEvidencePackSummaryFact | undefined,
session: AppServerAgentSessionFact,
exportedAt: string | undefined,
): AgentRuntimeExecutionEvent[] {
const packRoot = definedString(evidencePack?.packRelativeRoot);
if (!packRoot) {
return [];
}
return [
compactProjectionFields({
id: `appserver:evidence:${packRoot}`,
schemaVersion: "lime-runtime-event/v0.1",
kind: "evidence",
owner: "evidence",
status: "completed",
eventClass: "evidence.changed",
runtimeId: "app-server",
threadId: session.threadId,
evidenceId: packRoot,
evidenceRefs: [packRoot],
title: "Evidence pack exported",
detail: packRoot,
payload: compactProjectionFields({
packRelativeRoot: evidencePack?.packRelativeRoot,
packAbsoluteRoot: evidencePack?.packAbsoluteRoot,
exportedAt: evidencePack?.exportedAt ?? exportedAt,
threadStatus: evidencePack?.threadStatus,
latestTurnStatus: evidencePack?.latestTurnStatus,
turnCount: evidencePack?.turnCount,
itemCount: evidencePack?.itemCount,
pendingRequestCount: evidencePack?.pendingRequestCount,
queuedTurnCount: evidencePack?.queuedTurnCount,
recentArtifactCount: evidencePack?.recentArtifactCount,
knownGaps: evidencePack?.knownGaps,
artifacts: evidencePack?.artifacts,
}),
createdAt: evidencePack?.exportedAt ?? exportedAt ?? defaultTimestamp(),
completedAt: evidencePack?.exportedAt ?? exportedAt,
} satisfies AgentRuntimeExecutionEvent),
];
}
function normalizeEventClass(type: string): string {
if (type === "message.delta_batch" || type === "message.batch") {
return "model.delta";
}
if (type === "message.delta") return "model.delta";
if (
type === "message" ||
type === "message.completed" ||
type === "item.completed"
) {
return "model.completed";
}
if (type === "thinking.delta") return "reasoning.delta";
if (type === "artifact.snapshot") return "artifact.changed";
if (type === "runtime.status") return "run.status";
if (type === "turn.done" || type === "turn.final_done") {
return "turn.completed";
}
if (type === "turn.cancelled") return "turn.canceled";
return type;
}
function kindForEventClass(
eventClass: string,
): AgentRuntimeExecutionEventKind {
if (eventClass.startsWith("model.")) return "model";
if (eventClass.startsWith("reasoning.")) return "note";
if (eventClass.startsWith("tool.")) return "tool";
if (eventClass.startsWith("action.")) return "action";
if (eventClass.startsWith("artifact.")) return "draft";
if (eventClass.startsWith("evidence.")) return "evidence";
if (
eventClass.startsWith("subagent.") ||
eventClass.startsWith("handoff.") ||
eventClass.startsWith("review.") ||
eventClass.startsWith("task.")
) {
return "handoff";
}
return "state";
}
function statusForEventClass(
eventClass: string,
): AgentRuntimeExecutionEventStatus {
if (eventClass.endsWith(".failed") || eventClass === "runtime.error") {
return "failed";
}
if (eventClass === "action.required" || eventClass === "handoff.requested") {
return "pending";
}
if (
eventClass.endsWith(".completed") ||
eventClass.endsWith(".resolved") ||
eventClass.endsWith(".result") ||
eventClass.endsWith(".changed") ||
eventClass.endsWith(".verdict")
) {
return "completed";
}
if (eventClass === "turn.canceled") return "failed";
return "running";
}
function statusFromSessionStatus(
status: string | undefined,
): AgentRuntimeExecutionEventStatus {
if (status === "completed" || status === "idle") return "completed";
if (status === "failed" || status === "canceled") return "failed";
if (status === "waitingAction") return "blocked";
if (status === "running") return "running";
return "pending";
}
function statusFromTurnStatus(
status: string | undefined,
): AgentRuntimeExecutionEventStatus {
if (status === "completed") return "completed";
if (status === "failed" || status === "canceled") return "failed";
if (status === "waitingAction") return "pending";
if (status === "running" || status === "accepted") return "running";
return "pending";
}
function eventClassForTurnStatus(status: string | undefined): string {
if (status === "completed") return "turn.completed";
if (status === "failed") return "turn.failed";
if (status === "canceled") return "turn.canceled";
if (status === "waitingAction") return "action.required";
if (status === "running" || status === "accepted") return "turn.started";
return "turn.submitted";
}
function phaseForEventClass(eventClass: string): string {
if (eventClass === "action.required") return "action_required";
if (eventClass.endsWith(".failed") || eventClass === "runtime.error") {
return "failed";
}
if (
eventClass.endsWith(".completed") ||
eventClass.endsWith(".resolved") ||
eventClass.endsWith(".result") ||
eventClass.endsWith(".changed")
) {
return "completed";
}
if (eventClass.startsWith("tool.")) return "tool_running";
if (eventClass.startsWith("model.")) return "streaming";
return "preparing";
}
function ownerForEventClass(eventClass: string): string {
if (eventClass.startsWith("tool.")) return "runtime";
if (eventClass.startsWith("artifact.")) return "artifact";
if (eventClass.startsWith("evidence.")) return "evidence";
return "runtime";
}
function titleForEvent(
eventClass: string,
payload: Record<string, unknown>,
): string {
return (
readStringField(payload, ["title", "name", "toolName", "tool_name"]) ??
eventClass
);
}
function titleForTurnStatus(status: string | undefined): string {
if (status === "completed") return "Turn completed";
if (status === "failed") return "Turn failed";
if (status === "canceled") return "Turn canceled";
if (status === "waitingAction") return "Action required";
if (status === "queued") return "Turn queued";
return "Turn started";
}
function detailForEvent(
eventClass: string,
payload: Record<string, unknown>,
text: string | undefined,
): string | undefined {
if (eventClass.startsWith("model.") || eventClass.startsWith("reasoning.")) {
return text;
}
return readStringField(payload, ["detail", "message", "error", "reason"]);
}
function readText(payload: Record<string, unknown>): string | undefined {
const direct = readStringField(payload, ["text", "delta", "message"]);
if (direct) return direct;
const message = readRecord(payload.message);
const messageText = readStringField(message, ["text", "delta", "message"]);
if (messageText) return messageText;
const content = Array.isArray(message?.content)
? message.content
: Array.isArray(payload.content)
? payload.content
: [];
return content
.map((part) => readStringField(readRecord(part), ["text", "content"]))
.filter((value): value is string => Boolean(value))
.join("");
}
function readMessageId(payload: Record<string, unknown>): string | undefined {
const message = readRecord(payload.message);
return (
readStringField(payload, ["messageId", "message_id"]) ??
readStringField(message, ["id", "messageId", "message_id"])
);
}
function controlsForAction(payload: Record<string, unknown>): string[] {
const controls = readStringArrayField(payload, ["controls"]);
if (controls.length > 0) return controls;
const actionType = readStringField(payload, ["action_type", "actionType"]);
if (actionType === "ask_user" || actionType === "elicitation") {
return ["answer"];
}
if (actionType === "tool_confirmation") {
return ["approve", "reject"];
}
return [];
}
function isToolEvent(eventClass: string): boolean {
return eventClass.startsWith("tool.");
}
function isActionEvent(eventClass: string): boolean {
return eventClass.startsWith("action.");
}
function isCompletedEvent(eventClass: string): boolean {
return (
statusForEventClass(eventClass) === "completed" ||
statusForEventClass(eventClass) === "failed"
);
}
function normalizeRefs(values: Array<string | undefined>): string[] {
return Array.from(
new Set(
values
.map((value) => definedString(value))
.filter((value): value is string => Boolean(value)),
),
);
}
function dedupeExecutionEvents(
events: readonly AgentRuntimeExecutionEvent[],
): AgentRuntimeExecutionEvent[] {
const byId = new Map<string, AgentRuntimeExecutionEvent>();
events.forEach((event) => {
byId.set(event.id, event);
});
return Array.from(byId.values());
}
function compareEvents(
left: AgentRuntimeExecutionEvent,
right: AgentRuntimeExecutionEvent,
): number {
const leftSequence = readNumberField({ sequence: left.sequence }, ["sequence"]);
const rightSequence = readNumberField(
{ sequence: right.sequence },
["sequence"],
);
if (leftSequence !== undefined && rightSequence !== undefined) {
return leftSequence - rightSequence;
}
return left.createdAt.localeCompare(right.createdAt);
}
function defaultTimestamp(): string {
return "1970-01-01T00:00:00.000Z";
}
@@ -15,7 +15,9 @@ export type {
AgentRuntimeReadModel,
AgentRuntimeSurface,
AgentUiControl,
AgentUiArtifactRefView,
AgentUiDiagnosticView,
AgentUiEvidenceRefView,
AgentUiEventClass,
AgentUiHydrationStatus,
AgentUiOwner,
@@ -32,6 +34,11 @@ export type {
AgentUiRuntimeStatus,
AgentUiRuntimeStatusView,
AgentUiScope,
AgentUiSubagentActivityView,
AgentUiSubagentDelegationView,
AgentUiSubagentIsolationView,
AgentUiSubagentsModel,
AgentUiSubagentThreadView,
AgentUiSurface,
AgentUiTopology,
ExecutionGraph,
@@ -0,0 +1,131 @@
import {
collectAgentUiFixtureValidationIssues,
type AgentUiContractValidationIssue,
type AgentUiFixture,
type AgentUiProjectionState,
type AgentRuntimeExecutionEvent,
} from "@limecloud/agent-ui-contracts";
import { projectAgentUiState } from "./uiState.js";
export interface AgentUiFixtureReplayResult<
TEvent extends AgentRuntimeExecutionEvent = AgentRuntimeExecutionEvent,
> {
fixtureId: string;
state: AgentUiProjectionState<TEvent>;
validationIssues: AgentUiContractValidationIssue[];
diagnostics: string[];
passed: boolean;
}
export function replayAgentUiFixture<
TEvent extends AgentRuntimeExecutionEvent = AgentRuntimeExecutionEvent,
>(
fixture: AgentUiFixture<TEvent>,
): AgentUiFixtureReplayResult<TEvent> {
const validationIssues = collectAgentUiFixtureValidationIssues(fixture);
const state = projectAgentUiState<TEvent>({
executionEvents: fixture.events,
});
const diagnostics = collectReplayDiagnostics(fixture, state, validationIssues);
return {
fixtureId: fixture.id,
state,
validationIssues,
diagnostics,
passed: validationIssues.length === 0 && diagnostics.length === 0,
};
}
function collectReplayDiagnostics<
TEvent extends AgentRuntimeExecutionEvent,
>(
fixture: AgentUiFixture<TEvent>,
state: AgentUiProjectionState<TEvent>,
validationIssues: AgentUiContractValidationIssue[],
): string[] {
const diagnostics: string[] = validationIssues.map((issue) => issue.code);
const expected = fixture.expected;
if (expected.messagePartCount !== undefined && state.messages.length < expected.messagePartCount) {
diagnostics.push("message_parts_below_expected");
}
if (expected.timelineEntryCount !== undefined && state.timeline.length < expected.timelineEntryCount) {
diagnostics.push("timeline_entries_below_expected");
}
if (expected.graphNodeCount !== undefined && state.graph.length < expected.graphNodeCount) {
diagnostics.push("graph_nodes_below_expected");
}
if (expected.pendingActionCount !== undefined && state.readModel.pendingActions.length !== expected.pendingActionCount) {
diagnostics.push("pending_actions_mismatch");
}
if (expected.artifactCount !== undefined && state.artifacts.length < expected.artifactCount) {
diagnostics.push("artifacts_below_expected");
}
if (expected.evidenceCount !== undefined && state.evidence.length < expected.evidenceCount) {
diagnostics.push("evidence_below_expected");
}
diagnostics.push(...collectSubagentsReplayDiagnostics(fixture, state));
return diagnostics.filter(
(diagnostic) => !(expected.diagnostics ?? []).includes(diagnostic),
);
}
function collectSubagentsReplayDiagnostics<
TEvent extends AgentRuntimeExecutionEvent,
>(
fixture: AgentUiFixture<TEvent>,
state: AgentUiProjectionState<TEvent>,
): string[] {
const expected = fixture.expected.subagents;
if (!expected) {
return [];
}
const diagnostics: string[] = [];
const model = state.subagents;
if (
expected.hasSubagents !== undefined
&& model.hasSubagents !== expected.hasSubagents
) {
diagnostics.push("subagents_surface_mismatch");
}
if (
expected.threadCount !== undefined
&& model.threads.length < expected.threadCount
) {
diagnostics.push("subagent_threads_below_expected");
}
if (
expected.delegationCallCount !== undefined
&& model.delegationCalls.length < expected.delegationCallCount
) {
diagnostics.push("subagent_delegation_calls_below_expected");
}
if (
expected.activityCount !== undefined
&& model.activities.length < expected.activityCount
) {
diagnostics.push("subagent_activities_below_expected");
}
if (
expected.activeThreadCount !== undefined
&& model.activeThreadIds.length !== expected.activeThreadCount
) {
diagnostics.push("subagent_active_threads_mismatch");
}
if (
expected.completedThreadCount !== undefined
&& model.completedThreadIds.length !== expected.completedThreadCount
) {
diagnostics.push("subagent_completed_threads_mismatch");
}
if (
expected.failedThreadCount !== undefined
&& model.failedThreadIds.length !== expected.failedThreadCount
) {
diagnostics.push("subagent_failed_threads_mismatch");
}
return diagnostics;
}
+3
View File
@@ -1,11 +1,13 @@
export type * from "./contracts.js";
export * from "./actions.js";
export * from "./appServerFacts.js";
export * from "./artifactEvents.js";
export * from "./contextEvents.js";
export * from "./conversationEvents.js";
export * from "./diagnosticEvents.js";
export * from "./envelope.js";
export * from "./eventStore.js";
export * from "./fixtureReplay.js";
export * from "./hydrationEvents.js";
export * from "./lifecycle.js";
export * from "./normalization.js";
@@ -16,6 +18,7 @@ export * from "./refs.js";
export * from "./routing.js";
export * from "./runtimeFacts.js";
export * from "./summary.js";
export * from "./subagents.js";
export * from "./subagentStatusEvents.js";
export * from "./threadItems.js";
export * from "./toolEvents.js";
@@ -1,11 +1,13 @@
export * from "./contracts.js";
export * from "./actions.js";
export * from "./appServerFacts.js";
export * from "./artifactEvents.js";
export * from "./contextEvents.js";
export * from "./conversationEvents.js";
export * from "./diagnosticEvents.js";
export * from "./envelope.js";
export * from "./eventStore.js";
export * from "./fixtureReplay.js";
export * from "./hydrationEvents.js";
export * from "./lifecycle.js";
export * from "./normalization.js";
@@ -16,6 +18,7 @@ export * from "./refs.js";
export * from "./routing.js";
export * from "./runtimeFacts.js";
export * from "./summary.js";
export * from "./subagents.js";
export * from "./subagentStatusEvents.js";
export * from "./threadItems.js";
export * from "./toolEvents.js";
@@ -1,11 +1,13 @@
export type * from "./contracts.js";
export * from "./actions.js";
export * from "./appServerFacts.js";
export * from "./artifactEvents.js";
export * from "./contextEvents.js";
export * from "./conversationEvents.js";
export * from "./diagnosticEvents.js";
export * from "./envelope.js";
export * from "./eventStore.js";
export * from "./fixtureReplay.js";
export * from "./hydrationEvents.js";
export * from "./lifecycle.js";
export * from "./normalization.js";
@@ -16,6 +18,7 @@ export * from "./refs.js";
export * from "./routing.js";
export * from "./runtimeFacts.js";
export * from "./summary.js";
export * from "./subagents.js";
export * from "./subagentStatusEvents.js";
export * from "./threadItems.js";
export * from "./toolEvents.js";
@@ -0,0 +1,309 @@
import type {
AgentRuntimeExecutionEvent,
AgentUiSubagentActivityView,
AgentUiSubagentDelegationView,
AgentUiSubagentIsolationView,
AgentUiSubagentsModel,
AgentUiSubagentThreadView,
} from "@limecloud/agent-ui-contracts";
function isRecord(input: unknown): input is Record<string, unknown> {
return typeof input === "object" && input !== null && !Array.isArray(input);
}
function payloadString(
event: AgentRuntimeExecutionEvent,
...keys: string[]
): string | undefined {
const payload = event.payload;
if (!isRecord(payload)) return undefined;
for (const key of keys) {
const value = payload[key];
if (typeof value === "string" && value.trim()) {
return value;
}
}
return undefined;
}
function payloadNumber(
event: AgentRuntimeExecutionEvent,
...keys: string[]
): number | undefined {
const payload = event.payload;
if (!isRecord(payload)) return undefined;
for (const key of keys) {
const value = payload[key];
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
}
return undefined;
}
function payloadBoolean(
event: AgentRuntimeExecutionEvent,
...keys: string[]
): boolean | undefined {
const payload = event.payload;
if (!isRecord(payload)) return undefined;
for (const key of keys) {
const value = payload[key];
if (typeof value === "boolean") {
return value;
}
}
return undefined;
}
function safePreview(value: string | undefined): string | undefined {
if (!value) return undefined;
return value.length > 280 ? `${value.slice(0, 277)}...` : value;
}
function compact<T extends object>(input: T): T {
return Object.fromEntries(
Object.entries(input).filter(([, value]) => value !== undefined),
) as T;
}
function subagentThreadId(event: AgentRuntimeExecutionEvent): string | undefined {
return (
event.subagentId ??
event.workerId ??
payloadString(
event,
"subagentId",
"agentThreadId",
"childThreadId",
"childSessionId",
"session_id",
"targetThreadId",
)
);
}
function parentThreadId(event: AgentRuntimeExecutionEvent): string | undefined {
return (
payloadString(
event,
"parentThreadId",
"parent_thread_id",
"parentSessionId",
"parent_session_id",
) ?? event.threadId
);
}
function isSubagentEvent(event: AgentRuntimeExecutionEvent): boolean {
const eventClass = event.eventClass ?? "";
return Boolean(
subagentThreadId(event) ||
eventClass.startsWith("subagent.") ||
eventClass.startsWith("handoff.") ||
eventClass === "agent.spawned" ||
eventClass === "agent.completed",
);
}
function isDelegationEvent(event: AgentRuntimeExecutionEvent): boolean {
const eventClass = event.eventClass ?? "";
return (
eventClass.startsWith("subagent.") ||
eventClass.startsWith("handoff.") ||
eventClass === "agent.spawned" ||
eventClass === "agent.handoff"
);
}
function isTerminalStatus(status: string): boolean {
return status === "completed" || status === "failed";
}
function activityKind(event: AgentRuntimeExecutionEvent): string {
const eventClass = event.eventClass ?? "";
if (eventClass.includes("started") || eventClass === "agent.spawned") {
return "started";
}
if (eventClass.includes("completed") || eventClass === "agent.completed") {
return "completed";
}
if (eventClass.includes("failed")) return "failed";
if (eventClass.startsWith("handoff.")) return "handoff";
if (eventClass.startsWith("review.")) return "review";
if (eventClass.startsWith("tool.") || eventClass.startsWith("model.")) {
return "interacted";
}
return "updated";
}
function delegationAction(event: AgentRuntimeExecutionEvent): string {
const eventClass = event.eventClass ?? "";
if (eventClass.startsWith("handoff.") || eventClass === "agent.handoff") {
return "handoff";
}
if (eventClass.includes("wait")) return "wait";
if (eventClass.includes("interrupt")) return "interrupt";
if (eventClass.includes("close")) return "close";
if (eventClass.includes("send") || eventClass.includes("input")) {
return "send_input";
}
return "spawn";
}
function isolationForEvent(
event: AgentRuntimeExecutionEvent,
): AgentUiSubagentIsolationView | undefined {
const isolation = compact<AgentUiSubagentIsolationView>({
runtimeProfileId: payloadString(event, "runtimeProfileId", "runtime_profile_id"),
modelProfileId: payloadString(event, "modelProfileId", "model_profile_id"),
isolationProfileId: payloadString(
event,
"isolationProfileId",
"isolation_profile_id",
),
workspaceRef: payloadString(event, "workspaceRef", "workspace_ref", "cwdRef"),
permissionProfile: payloadString(
event,
"permissionProfile",
"permission_profile",
),
sandboxProfile: payloadString(event, "sandboxProfile", "sandbox_profile"),
forkPolicy: payloadString(event, "forkPolicy", "fork_policy"),
depth: payloadNumber(event, "depth", "subagentDepth"),
canDelegate: payloadBoolean(event, "canDelegate", "can_delegate"),
});
return Object.keys(isolation).length ? isolation : undefined;
}
function mergeThread(
current: AgentUiSubagentThreadView | undefined,
event: AgentRuntimeExecutionEvent,
threadId: string,
): AgentUiSubagentThreadView {
const sourceEventIds = new Set([
...(current?.sourceEventIds ?? []),
event.id,
]);
const artifactRefs = new Set([
...(current?.artifactRefs ?? []),
...(event.artifactRefs ?? []),
]);
const evidenceRefs = new Set([
...(current?.evidenceRefs ?? []),
...(event.evidenceRefs ?? []),
]);
return compact<AgentUiSubagentThreadView>({
threadId,
subagentId: event.subagentId ?? current?.subagentId ?? threadId,
parentThreadId: current?.parentThreadId ?? parentThreadId(event),
parentTaskId:
current?.parentTaskId ??
payloadString(event, "parentTaskId", "parent_task_id"),
taskId: current?.taskId ?? event.taskId,
taskPath:
current?.taskPath ??
payloadString(event, "taskPath", "task_path", "agentPath", "agent_path"),
role:
current?.role ??
payloadString(event, "role", "agentRole", "agent_role"),
nickname:
current?.nickname ??
payloadString(event, "nickname", "agentNickname", "agent_nickname"),
status: event.status,
title: current?.title ?? event.title,
summary:
payloadString(event, "summary", "resultSummary", "outputSummary") ??
current?.summary,
promptPreview:
current?.promptPreview ??
safePreview(
payloadString(event, "promptPreview", "prompt_preview", "prompt", "input"),
),
lastActivityAt: event.completedAt ?? event.createdAt ?? current?.lastActivityAt,
createdAt: current?.createdAt ?? event.createdAt,
completedAt: event.completedAt ?? (isTerminalStatus(event.status) ? event.createdAt : current?.completedAt),
artifactRefs: Array.from(artifactRefs),
evidenceRefs: Array.from(evidenceRefs),
sourceEventIds: Array.from(sourceEventIds),
isolation: current?.isolation ?? isolationForEvent(event),
});
}
function delegationForEvent(
event: AgentRuntimeExecutionEvent,
threadId: string,
): AgentUiSubagentDelegationView {
const parentId = parentThreadId(event);
return compact<AgentUiSubagentDelegationView>({
callId:
event.handoffId ??
payloadString(event, "callId", "delegationCallId") ??
`${parentId ?? "root"}:delegation:${threadId}`,
sourceEventId: event.id,
action: delegationAction(event),
parentThreadId: parentId,
targetThreadIds: [threadId],
status: event.status,
title: event.title,
promptPreview: safePreview(
payloadString(event, "promptPreview", "prompt_preview", "prompt", "input"),
),
createdAt: event.createdAt,
completedAt: event.completedAt,
});
}
function activityForEvent(
event: AgentRuntimeExecutionEvent,
threadId: string,
): AgentUiSubagentActivityView {
return compact<AgentUiSubagentActivityView>({
activityId: event.id,
threadId,
sourceEventId: event.id,
kind: activityKind(event),
status: event.status,
title: event.title,
createdAt: event.createdAt,
});
}
export function buildAgentUiSubagentsModel(
events: readonly AgentRuntimeExecutionEvent[] = [],
): AgentUiSubagentsModel {
const threads = new Map<string, AgentUiSubagentThreadView>();
const delegationCalls = new Map<string, AgentUiSubagentDelegationView>();
const activities: AgentUiSubagentActivityView[] = [];
events.forEach((event) => {
if (!isSubagentEvent(event)) return;
const threadId = subagentThreadId(event);
if (!threadId) return;
threads.set(threadId, mergeThread(threads.get(threadId), event, threadId));
activities.push(activityForEvent(event, threadId));
if (isDelegationEvent(event)) {
const delegation = delegationForEvent(event, threadId);
delegationCalls.set(delegation.callId, delegation);
}
});
const threadList = Array.from(threads.values());
return {
hasSubagents: threadList.length > 0,
threads: threadList,
delegationCalls: Array.from(delegationCalls.values()),
activities,
activeThreadIds: threadList
.filter((thread) => thread.status === "pending" || thread.status === "running" || thread.status === "blocked")
.map((thread) => thread.threadId),
completedThreadIds: threadList
.filter((thread) => thread.status === "completed")
.map((thread) => thread.threadId),
failedThreadIds: threadList
.filter((thread) => thread.status === "failed")
.map((thread) => thread.threadId),
};
}
@@ -15,7 +15,7 @@ export interface AgentUiProjectionSummary {
latestNotableEvents: AgentUiProjectionEvent[];
}
export interface AgentUiTeamWorkbenchProjectionSummary {
export interface AgentUiSubagentsProjectionSummary {
total: number;
rosterCount: number;
workBoardCount: number;
@@ -30,27 +30,25 @@ export interface AgentUiTeamWorkbenchProjectionSummary {
latestEvents: AgentUiProjectionEvent[];
}
export interface AgentUiTeamWorkbenchSurfaceLaneDefinition {
export interface AgentUiSubagentsSurfaceLaneDefinition {
id: string;
label: string;
description: string;
surfaces: AgentUiSurface[];
}
export interface AgentUiTeamWorkbenchSurfaceLaneSummary
extends AgentUiTeamWorkbenchSurfaceLaneDefinition {
export interface AgentUiSubagentsSurfaceLaneSummary extends AgentUiSubagentsSurfaceLaneDefinition {
total: number;
latestEvents: AgentUiProjectionEvent[];
}
export interface AgentUiTeamWorkbenchSurfaceDefinition {
export interface AgentUiSubagentsSurfaceDefinition {
surface: AgentUiSurface;
label: string;
description: string;
}
export interface AgentUiTeamWorkbenchSurfaceSummary
extends AgentUiTeamWorkbenchSurfaceDefinition {
export interface AgentUiSubagentsSurfaceSummary extends AgentUiSubagentsSurfaceDefinition {
total: number;
latestEvents: AgentUiProjectionEvent[];
}
@@ -66,7 +64,7 @@ export const EMPTY_AGENT_UI_PROJECTION_SUMMARY: AgentUiProjectionSummary = {
latestNotableEvents: [],
};
export const EMPTY_AGENT_UI_TEAM_WORKBENCH_PROJECTION_SUMMARY: AgentUiTeamWorkbenchProjectionSummary =
export const EMPTY_AGENT_UI_SUBAGENTS_PROJECTION_SUMMARY: AgentUiSubagentsProjectionSummary =
{
total: 0,
rosterCount: 0,
@@ -127,7 +125,7 @@ export const AGENT_UI_EVIDENCE_EVENT_TYPES = new Set<AgentUiEventClass>([
"review.completed",
]);
export const AGENT_UI_TEAM_WORKBENCH_SURFACES = new Set<AgentUiSurface>([
export const AGENT_UI_SUBAGENTS_SURFACES = new Set<AgentUiSurface>([
"team_roster",
"work_board",
"delegation_graph",
@@ -140,7 +138,7 @@ export const AGENT_UI_TEAM_WORKBENCH_SURFACES = new Set<AgentUiSurface>([
"team_policy",
]);
export const AGENT_UI_TEAM_WORKBENCH_SURFACE_DEFINITIONS: AgentUiTeamWorkbenchSurfaceDefinition[] =
export const AGENT_UI_SUBAGENTS_SURFACE_DEFINITIONS: AgentUiSubagentsSurfaceDefinition[] =
[
{
surface: "team_roster",
@@ -175,7 +173,7 @@ export const AGENT_UI_TEAM_WORKBENCH_SURFACE_DEFINITIONS: AgentUiTeamWorkbenchSu
{
surface: "teammate_transcript",
label: "Transcript",
description: "队友 transcript ref 与局部会话线索",
description: "子代理 transcript ref 与局部会话线索",
},
{
surface: "background_teammate",
@@ -185,7 +183,7 @@ export const AGENT_UI_TEAM_WORKBENCH_SURFACE_DEFINITIONS: AgentUiTeamWorkbenchSu
{
surface: "remote_teammate",
label: "Remote",
description: "远端 teammate / external task 状态",
description: "远端子代理 / external task 状态",
},
{
surface: "team_policy",
@@ -194,12 +192,12 @@ export const AGENT_UI_TEAM_WORKBENCH_SURFACE_DEFINITIONS: AgentUiTeamWorkbenchSu
},
];
export const AGENT_UI_TEAM_WORKBENCH_SURFACE_LANES: AgentUiTeamWorkbenchSurfaceLaneDefinition[] =
export const AGENT_UI_SUBAGENTS_SURFACE_LANES: AgentUiSubagentsSurfaceLaneDefinition[] =
[
{
id: "team-topology",
label: "Team 拓扑",
description: "成员、分派、任务板与策略事实",
label: "Subagents 拓扑",
description: "子代理、分派、任务板与策略事实",
surfaces: [
"team_roster",
"delegation_graph",
@@ -210,7 +208,7 @@ export const AGENT_UI_TEAM_WORKBENCH_SURFACE_LANES: AgentUiTeamWorkbenchSurfaceL
{
id: "worker-flow",
label: "Worker 流",
description: "执行通知、队友 transcript 与后台/远端伙伴",
description: "执行通知、子代理 transcript 与后台/远端子代理",
surfaces: [
"worker_notifications",
"teammate_transcript",
@@ -226,7 +224,7 @@ export const AGENT_UI_TEAM_WORKBENCH_SURFACE_LANES: AgentUiTeamWorkbenchSurfaceL
},
];
export const AGENT_UI_TEAM_WORKBENCH_EVENT_TYPES = new Set<AgentUiEventClass>([
export const AGENT_UI_SUBAGENTS_EVENT_TYPES = new Set<AgentUiEventClass>([
"agent.changed",
"agent.spawned",
"agent.completed",
@@ -293,56 +291,59 @@ export function summarizeAgentUiProjectionEvents(
};
}
export function summarizeAgentUiTeamWorkbenchProjectionEvents(
export function summarizeAgentUiSubagentsProjectionEvents(
events: AgentUiProjectionEvent[],
): AgentUiTeamWorkbenchProjectionSummary {
const teamEvents = events.filter(
): AgentUiSubagentsProjectionSummary {
const subagentEvents = events.filter(
(event) =>
AGENT_UI_TEAM_WORKBENCH_EVENT_TYPES.has(event.type) ||
Boolean(
event.surface && AGENT_UI_TEAM_WORKBENCH_SURFACES.has(event.surface),
),
AGENT_UI_SUBAGENTS_EVENT_TYPES.has(event.type) ||
Boolean(event.surface && AGENT_UI_SUBAGENTS_SURFACES.has(event.surface)),
);
if (teamEvents.length === 0) {
return EMPTY_AGENT_UI_TEAM_WORKBENCH_PROJECTION_SUMMARY;
if (subagentEvents.length === 0) {
return EMPTY_AGENT_UI_SUBAGENTS_PROJECTION_SUMMARY;
}
return {
total: teamEvents.length,
rosterCount: teamEvents.filter((event) => event.surface === "team_roster")
.length,
workBoardCount: teamEvents.filter((event) => event.surface === "work_board")
.length,
delegationCount: teamEvents.filter(
total: subagentEvents.length,
rosterCount: subagentEvents.filter(
(event) => event.surface === "team_roster",
).length,
workBoardCount: subagentEvents.filter(
(event) => event.surface === "work_board",
).length,
delegationCount: subagentEvents.filter(
(event) => event.surface === "delegation_graph",
).length,
handoffCount: teamEvents.filter((event) => event.surface === "handoff_lane")
.length,
workerNotificationCount: teamEvents.filter(
handoffCount: subagentEvents.filter(
(event) => event.surface === "handoff_lane",
).length,
workerNotificationCount: subagentEvents.filter(
(event) => event.surface === "worker_notifications",
).length,
reviewCount: teamEvents.filter((event) => event.surface === "review_lane")
.length,
transcriptCount: teamEvents.filter(
reviewCount: subagentEvents.filter(
(event) => event.surface === "review_lane",
).length,
transcriptCount: subagentEvents.filter(
(event) => event.surface === "teammate_transcript",
).length,
backgroundCount: teamEvents.filter(
backgroundCount: subagentEvents.filter(
(event) => event.surface === "background_teammate",
).length,
remoteCount: teamEvents.filter(
remoteCount: subagentEvents.filter(
(event) => event.surface === "remote_teammate",
).length,
policyCount: teamEvents.filter((event) => event.surface === "team_policy")
.length,
latestEvents: teamEvents.slice().reverse().slice(0, 5),
policyCount: subagentEvents.filter(
(event) => event.surface === "team_policy",
).length,
latestEvents: subagentEvents.slice().reverse().slice(0, 5),
};
}
export function summarizeAgentUiTeamWorkbenchSurfaceLanes(
export function summarizeAgentUiSubagentsSurfaceLanes(
events: AgentUiProjectionEvent[],
): AgentUiTeamWorkbenchSurfaceLaneSummary[] {
return AGENT_UI_TEAM_WORKBENCH_SURFACE_LANES.map((lane) => {
): AgentUiSubagentsSurfaceLaneSummary[] {
return AGENT_UI_SUBAGENTS_SURFACE_LANES.map((lane) => {
const surfaceSet = new Set(lane.surfaces);
const laneEvents = events.filter(
(event) => event.surface && surfaceSet.has(event.surface),
@@ -356,12 +357,12 @@ export function summarizeAgentUiTeamWorkbenchSurfaceLanes(
}).filter((lane) => lane.total > 0);
}
export function summarizeAgentUiTeamWorkbenchSurfaces(
export function summarizeAgentUiSubagentsSurfaces(
events: AgentUiProjectionEvent[],
options: { latestLimit?: number } = {},
): AgentUiTeamWorkbenchSurfaceSummary[] {
): AgentUiSubagentsSurfaceSummary[] {
const latestLimit = Math.max(1, options.latestLimit ?? 3);
return AGENT_UI_TEAM_WORKBENCH_SURFACE_DEFINITIONS.map((definition) => {
return AGENT_UI_SUBAGENTS_SURFACE_DEFINITIONS.map((definition) => {
const surfaceEvents = events.filter(
(event) => event.surface === definition.surface,
);
+192 -10
View File
@@ -1,8 +1,12 @@
import type {
AgentRuntimeExecutionEvent,
AgentRuntimeProjectionInput,
AgentRuntimeReadModel,
AgentUiArtifactRefView,
AgentUiEvidenceRefView,
AgentUiProjectionState,
AgentUiProjector,
AgentUiRefView,
AgentUiRuntimeStatusView,
ExecutionGraphNode,
ExecutionGraphNodeType,
@@ -11,6 +15,7 @@ import type {
UIMessagePart,
} from "@limecloud/agent-ui-contracts";
import { agentEventSurface, projectAgentRuntimeReadModel } from "./readModel.js";
import { buildAgentUiSubagentsModel } from "./subagents.js";
function eventRefs(event: AgentRuntimeExecutionEvent): string[] {
return [
@@ -20,6 +25,122 @@ function eventRefs(event: AgentRuntimeExecutionEvent): string[] {
];
}
function isRecord(input: unknown): input is Record<string, unknown> {
return typeof input === "object" && input !== null && !Array.isArray(input);
}
export interface AgentUiSessionSnapshotMessage {
id: string;
role?: "user" | "assistant" | "system" | string;
content?: string;
text?: string;
createdAt?: string;
refs?: string[];
}
export interface AgentUiSessionSnapshotInput<
TEvent extends AgentRuntimeExecutionEvent = AgentRuntimeExecutionEvent,
> extends AgentRuntimeProjectionInput<TEvent> {
messages?: readonly AgentUiSessionSnapshotMessage[];
readModel?: AgentRuntimeReadModel<TEvent>;
}
function payloadString(
event: AgentRuntimeExecutionEvent,
...keys: string[]
): string | undefined {
const payload = event.payload;
if (!isRecord(payload)) return undefined;
for (const key of keys) {
const value = payload[key];
if (typeof value === "string" && value.trim()) {
return value;
}
}
return undefined;
}
function payloadRecord(
event: AgentRuntimeExecutionEvent,
key: string,
): Record<string, unknown> | undefined {
const payload = event.payload;
if (!isRecord(payload)) return undefined;
const value = payload[key];
return isRecord(value) ? value : undefined;
}
function safeRefPath(value: string | undefined): string | undefined {
if (!value) return undefined;
if (/^(?:[A-Za-z]:[\\/]|\/|file:)/.test(value)) return undefined;
if (/(?:api[-_]?key|authorization|password|secret|token)=/i.test(value)) {
return undefined;
}
return value;
}
function safePreview(value: string | undefined): string | undefined {
if (!value) return undefined;
return value.length > 280 ? `${value.slice(0, 277)}...` : value;
}
function compactRefView(ref: AgentUiRefView): AgentUiRefView {
return Object.fromEntries(
Object.entries(ref).filter(([, value]) => value !== undefined),
) as AgentUiRefView;
}
function refViewForEvent(
event: AgentRuntimeExecutionEvent,
refId: string,
): AgentUiRefView {
const payloadSourceEventId = payloadString(event, "sourceEventId", "eventId");
const path = safeRefPath(
payloadString(
event,
"path",
"relativePath",
"packRelativeRoot",
"artifactPath",
"evidencePath",
),
);
return compactRefView({
id: refId,
sourceEventId: payloadSourceEventId ?? event.id,
title: payloadString(event, "title", "name") ?? event.title,
status: event.status,
owner: event.owner,
path,
contentRef: payloadString(event, "contentRef", "uri", "url"),
mimeType: payloadString(event, "mimeType", "mime"),
preview: safePreview(payloadString(event, "preview", "summary", "text")),
metadata: payloadRecord(event, "metadata"),
});
}
function collectRefViews(
events: AgentRuntimeExecutionEvent[],
readModelRefs: string[],
kind: "artifact" | "evidence",
): AgentUiRefView[] {
const refs = new Map<string, AgentUiRefView>();
events.forEach((event) => {
const eventRefs = kind === "artifact"
? event.artifactRefs ?? []
: event.evidenceRefs ?? [];
eventRefs.forEach((id) => {
if (!refs.has(id)) refs.set(id, refViewForEvent(event, id));
});
});
readModelRefs.forEach((id) => {
if (!refs.has(id)) {
refs.set(id, { id, sourceEventId: id });
}
});
return Array.from(refs.values());
}
function uiPartForEvent(
event: AgentRuntimeExecutionEvent,
): UIMessagePart | undefined {
@@ -191,6 +312,38 @@ function collectMessageParts(
return parts;
}
function normalizeSnapshotMessageRole(
role: AgentUiSessionSnapshotMessage["role"],
): string {
return role && role.trim() ? role : "assistant";
}
function snapshotMessageText(message: AgentUiSessionSnapshotMessage): string {
return message.content ?? message.text ?? "";
}
function messagePartForSnapshotMessage(
message: AgentUiSessionSnapshotMessage,
): UIMessagePart {
return {
type: "text",
partId: `message:${message.id}`,
messageId: message.id,
role: normalizeSnapshotMessageRole(message.role),
text: snapshotMessageText(message),
state: "final",
sourceEventId: `message:${message.id}`,
createdAt: message.createdAt,
refs: message.refs,
};
}
function collectExecutionEventsFromReadModel<TEvent extends AgentRuntimeExecutionEvent>(
readModel?: AgentRuntimeReadModel<TEvent>,
): TEvent[] {
return readModel?.events.map((event) => event.source) ?? [];
}
function timelineKindForEvent(
event: AgentRuntimeExecutionEvent,
): ProcessTimelineEntryKind {
@@ -230,6 +383,7 @@ function graphNodeIdForEvent(
event: AgentRuntimeExecutionEvent,
): string | undefined {
return (
event.subagentId ??
event.toolCallId ??
event.actionId ??
event.stepId ??
@@ -243,6 +397,7 @@ function graphNodeIdForEvent(
function graphNodeTypeForEvent(
event: AgentRuntimeExecutionEvent,
): ExecutionGraphNodeType {
if (event.subagentId) return "subagent";
if (event.toolCallId) return "tool";
if (event.actionId) return "action";
if (event.stepId) return "step";
@@ -268,7 +423,9 @@ function upsertGraphNode(
nodes.set(nodeId, {
nodeId,
parentId:
event.stepId && event.toolCallId
event.subagentId && event.taskId
? event.taskId
: event.stepId && event.toolCallId
? event.stepId
: event.taskId && event.runId
? event.taskId
@@ -357,6 +514,7 @@ export function projectAgentUiState<
const timeline = executionEvents.map(timelineEntryForEvent);
const graphNodes = new Map<string, ExecutionGraphNode>();
executionEvents.forEach((event) => upsertGraphNode(graphNodes, event));
const graph = Array.from(graphNodes.values());
const actions = readModel.events.filter(
(event) => event.surface === "human-action",
);
@@ -379,18 +537,21 @@ export function projectAgentUiState<
runtime: runtimeStatusForEvents(executionEvents),
messages,
timeline,
graph: Array.from(graphNodes.values()),
graph,
tools,
actions,
artifacts: readModel.artifactRefs.map((id) => ({
id,
sourceEventId: id,
})),
evidence: readModel.evidenceRefs.map((id) => ({
id,
sourceEventId: id,
})),
artifacts: collectRefViews(
executionEvents,
readModel.artifactRefs,
"artifact",
) as AgentUiArtifactRefView[],
evidence: collectRefViews(
executionEvents,
readModel.evidenceRefs,
"evidence",
) as AgentUiEvidenceRefView[],
diagnostics,
subagents: buildAgentUiSubagentsModel(executionEvents),
readModel,
hydration: {
status: executionEvents.length ? "live" : "idle",
@@ -400,6 +561,27 @@ export function projectAgentUiState<
};
}
export function projectAgentUiStateFromSessionSnapshot<
TEvent extends AgentRuntimeExecutionEvent,
>(
input: AgentUiSessionSnapshotInput<TEvent> = {},
): AgentUiProjectionState<TEvent> {
const executionEvents =
input.executionEvents ?? collectExecutionEventsFromReadModel(input.readModel);
const state = projectAgentUiState<TEvent>({
executionEvents,
sourceCount: input.sourceCount ?? input.readModel?.sourceCount,
});
const messages = input.messages?.map(messagePartForSnapshotMessage) ?? [];
if (!messages.length) {
return state;
}
return {
...state,
messages,
};
}
export function createAgentUiProjector<
TEvent extends AgentRuntimeExecutionEvent,
>(initialInput?: AgentRuntimeProjectionInput<TEvent>): AgentUiProjector<TEvent> {
@@ -0,0 +1,219 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
projectAppServerEventsToExecutionEvents,
projectAppServerSessionReadToExecutionEvents,
replayAppServerFacts,
} from "../dist/index.js";
const timestamp = "2026-06-10T00:00:00.000Z";
test("App Server events replay into standard projection surfaces", () => {
const result = replayAppServerFacts({
events: [
appServerEvent("evt-message", 1, "message.delta", {
text: "你好,Lime。",
messageId: "msg-1",
}),
appServerEvent("evt-tool", 2, "tool.started", {
toolCallId: "tool-1",
toolName: "search",
}),
appServerEvent("evt-action", 3, "action.required", {
request_id: "request-1",
action_type: "tool_confirmation",
}),
appServerEvent("evt-artifact", 4, "artifact.snapshot", {
artifactRef: "artifact-1",
artifactId: "artifact-1",
path: "draft.md",
}),
appServerEvent("evt-evidence", 5, "evidence.changed", {
evidenceRef: "evidence-1",
}),
appServerEvent("evt-completed", 6, "turn.completed", {
text: "完成",
}),
],
});
assert.deepEqual(result.diagnostics, []);
assert.equal(result.state.runtime.status, "completed");
assert.equal(result.state.messages[0].text, "你好,Lime。");
assert.equal(result.state.tools.length, 1);
assert.equal(result.state.readModel.pendingActions.length, 1);
assert.deepEqual(
result.state.readModel.pendingActions[0].actions.map(
(action) => action.decision,
),
["approve", "reject"],
);
assert.deepEqual(
result.state.artifacts.map((artifact) => artifact.id),
["artifact-1"],
);
assert.deepEqual(
result.state.evidence.map((evidence) => evidence.id),
["evidence-1"],
);
});
test("App Server read model hydrates snapshot and turn lifecycle facts", () => {
const events = projectAppServerSessionReadToExecutionEvents({
session: {
sessionId: "session-1",
threadId: "thread-1",
appId: "content-studio",
status: "running",
createdAt: timestamp,
updatedAt: "2026-06-10T00:00:02.000Z",
},
turns: [
{
turnId: "turn-1",
sessionId: "session-1",
threadId: "thread-1",
status: "completed",
startedAt: timestamp,
completedAt: "2026-06-10T00:00:01.000Z",
},
{
turnId: "turn-2",
sessionId: "session-1",
threadId: "thread-1",
status: "waitingAction",
startedAt: "2026-06-10T00:00:02.000Z",
},
],
});
assert.equal(events.length, 3);
assert.equal(events[0].eventClass, "snapshot.updated");
assert.equal(events[1].eventClass, "turn.completed");
assert.equal(events[2].eventClass, "action.required");
const result = replayAppServerFacts({
readModel: {
session: {
sessionId: "session-1",
threadId: "thread-1",
status: "running",
updatedAt: timestamp,
},
turns: [
{
turnId: "turn-2",
status: "waitingAction",
},
],
},
});
assert.equal(result.state.runtime.status, "waiting");
assert.equal(result.state.readModel.pendingActions.length, 1);
assert.equal(
result.state.graph.some(
(node) => node.nodeId === "turn-2" && node.nodeType === "turn",
),
true,
);
});
test("App Server evidence export projects artifacts, evidence pack and deduped events", () => {
const duplicatedEvent = appServerEvent("evt-message", 1, "message.delta", {
text: "导出文本",
});
const result = replayAppServerFacts({
events: [duplicatedEvent],
evidenceExport: {
session: {
sessionId: "session-1",
threadId: "thread-1",
status: "completed",
updatedAt: timestamp,
},
turns: [
{
turnId: "turn-1",
status: "completed",
completedAt: timestamp,
},
],
events: [duplicatedEvent],
artifacts: [
{
artifactRef: "artifact-document:req-1",
eventId: "evt-artifact",
sequence: 8,
turnId: "turn-1",
artifactId: "artifact-1",
path: "draft.md",
title: "Draft",
contentStatus: "notRequested",
},
],
exportedAt: "2026-06-10T00:00:03.000Z",
evidencePack: {
packRelativeRoot: ".lime/harness/sessions/session-1",
exportedAt: "2026-06-10T00:00:03.000Z",
threadStatus: "completed",
latestTurnStatus: "completed",
turnCount: 1,
itemCount: 2,
pendingRequestCount: 0,
queuedTurnCount: 0,
recentArtifactCount: 1,
knownGaps: [],
artifacts: [
{
kind: "markdown",
title: "Summary",
relativePath: "summary.md",
bytes: 128,
},
],
},
},
});
assert.equal(
result.events.filter((event) => event.id === "appserver:evt-message")
.length,
1,
);
assert.deepEqual(
result.state.artifacts.map((artifact) => artifact.id),
["artifact-document:req-1"],
);
assert.deepEqual(
result.state.evidence.map((evidence) => evidence.id),
[".lime/harness/sessions/session-1"],
);
assert.equal(result.state.hydration.eventCount, result.events.length);
});
test("App Server event adapter keeps projection package runtime-client free", () => {
const events = projectAppServerEventsToExecutionEvents([
appServerEvent("evt-1", 1, "turn.failed", {
message: "boom",
}),
]);
assert.equal(events[0].eventClass, "turn.failed");
assert.equal(events[0].status, "failed");
assert.equal(events[0].detail, "boom");
});
function appServerEvent(eventId, sequence, type, payload = {}) {
return {
eventId,
sequence,
sessionId: "session-1",
threadId: "thread-1",
turnId: "turn-1",
type,
timestamp,
payload,
};
}
@@ -0,0 +1,87 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
agentUiConformanceFixtures,
getAgentUiFixture,
} from "../../agent-ui-contracts/dist/index.js";
import {
createAgentUiProjector,
replayAgentUiFixture,
} from "../dist/index.js";
test("replayAgentUiFixture replays standard contract fixtures", () => {
for (const fixture of agentUiConformanceFixtures) {
const result = replayAgentUiFixture(fixture);
assert.equal(result.validationIssues.length, 0, fixture.id);
assert.equal(result.passed, true, fixture.id);
assert.equal(result.state.hydration.eventCount, fixture.events.length);
assert.equal(
result.state.runtime.latestEventId,
fixture.events.at(-1)?.id,
fixture.id,
);
}
});
test("replayAgentUiFixture proves key projection surfaces", () => {
const textState = replayAgentUiFixture(getAgentUiFixture("text-basic")).state;
assert.ok(textState.messages.length >= 1);
assert.ok(textState.messages.some((part) => part.text === "你好,Lime。"));
assert.equal(
replayAgentUiFixture(getAgentUiFixture("tool-success")).state.tools.length,
2,
);
assert.equal(
replayAgentUiFixture(getAgentUiFixture("hitl-action")).state.actions.length,
2,
);
assert.deepEqual(
replayAgentUiFixture(getAgentUiFixture("artifact-evidence")).state.artifacts
.map((artifact) => artifact.id),
["artifact_fixture_1"],
);
assert.deepEqual(
replayAgentUiFixture(getAgentUiFixture("artifact-evidence")).state.evidence
.map((evidence) => evidence.id),
["evidence_fixture_1"],
);
const handoffState = replayAgentUiFixture(getAgentUiFixture("subagent-handoff")).state;
assert.equal(handoffState.subagents.hasSubagents, true);
assert.deepEqual(
handoffState.subagents.threads.map((thread) => thread.threadId),
["subagent_fixture_researcher"],
);
assert.deepEqual(
handoffState.subagents.delegationCalls.map((delegation) => delegation.action),
["spawn", "handoff"],
);
assert.deepEqual(
handoffState.subagents.activities.map((activity) => activity.kind),
["started", "interacted", "handoff", "completed"],
);
});
test("projector apply remains idempotent for fixture replay", () => {
const fixture = getAgentUiFixture("subagent-handoff");
const projector = createAgentUiProjector();
for (const event of fixture.events) {
projector.apply(event);
projector.apply(event);
}
const state = projector.getState();
assert.equal(state.hydration.eventCount, fixture.events.length);
assert.ok(
state.graph.some(
(node) =>
node.nodeId === "subagent_fixture_researcher"
&& node.nodeType === "subagent",
),
);
assert.ok(
state.timeline.some((entry) => entry.sourceEventId === "evt_handoff_requested"),
);
});
@@ -46,6 +46,7 @@ import {
normalizeRuntimeStatusFromRuntimePhase,
normalizeSubagentRuntimeStatus,
projectAgentUiState,
projectAgentUiStateFromSessionSnapshot,
projectAgentRuntimeReadModel,
readNumberField,
readRecord,
@@ -72,9 +73,9 @@ import {
buildRoutingDecisionPayload,
extractArtifactRefs,
summarizeAgentUiProjectionEvents,
summarizeAgentUiTeamWorkbenchProjectionEvents,
summarizeAgentUiTeamWorkbenchSurfaceLanes,
summarizeAgentUiTeamWorkbenchSurfaces,
summarizeAgentUiSubagentsProjectionEvents,
summarizeAgentUiSubagentsSurfaceLanes,
summarizeAgentUiSubagentsSurfaces,
truncateText,
} from "../dist/index.js";
@@ -247,11 +248,112 @@ test("projectAgentUiState exposes standard message, timeline and graph projectio
);
assert.equal(state.actions.length, 1);
assert.deepEqual(state.artifacts, [
{ id: "artifact-1", sourceEventId: "artifact-1" },
{
id: "artifact-1",
sourceEventId: "evt-tool",
title: "读取资料",
status: "completed",
},
]);
assert.equal(state.hydration.status, "live");
});
test("projectAgentUiState builds structured artifact and evidence refs", () => {
const state = projectAgentUiState({
executionEvents: [
{
id: "evt-artifact",
kind: "draft",
status: "completed",
owner: "artifact",
eventClass: "artifact.changed",
title: "草稿已保存",
artifactRefs: ["prompt-draft:1"],
payload: {
sourceEventId: "provider-artifact-1",
relativePath: "drafts/prompt-1.md",
contentRef: "content://prompt-draft/1",
mimeType: "text/markdown",
preview: "可编辑 Prompt 草稿",
metadata: { draftId: "1" },
},
sequence: 1,
createdAt: "2026-06-07T00:00:00.000Z",
},
{
id: "evt-evidence",
kind: "evidence",
status: "completed",
owner: "evidence",
eventClass: "evidence.changed",
title: "输入资料已引用",
evidenceRefs: ["input-source:1"],
payload: {
sourceEventId: "provider-evidence-1",
packRelativeRoot: "evidence/input-source-1",
summary: "资料来源摘要",
mime: "application/json",
metadata: { sourceId: "1" },
},
sequence: 2,
createdAt: "2026-06-07T00:00:01.000Z",
},
],
});
assert.deepEqual(state.artifacts, [
{
id: "prompt-draft:1",
sourceEventId: "provider-artifact-1",
title: "草稿已保存",
status: "completed",
owner: "artifact",
path: "drafts/prompt-1.md",
contentRef: "content://prompt-draft/1",
mimeType: "text/markdown",
preview: "可编辑 Prompt 草稿",
metadata: { draftId: "1" },
},
]);
assert.deepEqual(state.evidence, [
{
id: "input-source:1",
sourceEventId: "provider-evidence-1",
title: "输入资料已引用",
status: "completed",
owner: "evidence",
path: "evidence/input-source-1",
mimeType: "application/json",
preview: "资料来源摘要",
metadata: { sourceId: "1" },
},
]);
});
test("projectAgentUiState keeps unsafe ref paths out of the UI contract", () => {
const state = projectAgentUiState({
executionEvents: [
{
id: "evt-artifact",
kind: "draft",
status: "completed",
eventClass: "artifact.changed",
title: "草稿已保存",
artifactRefs: ["prompt-draft:1"],
payload: {
path: "/Users/private/workspace/draft.md",
preview: "x".repeat(320),
},
sequence: 1,
createdAt: "2026-06-07T00:00:00.000Z",
},
],
});
assert.equal(state.artifacts[0].path, undefined);
assert.equal(state.artifacts[0].preview.length, 280);
});
test("projectAgentUiState merges streaming text and reasoning parts by scope", () => {
const state = projectAgentUiState({
executionEvents: [
@@ -383,6 +485,81 @@ test("projectAgentUiState keeps unresolved actions in waiting status", () => {
assert.equal(state.runtime.status, "waiting");
});
test("projectAgentUiStateFromSessionSnapshot upgrades legacy transcript plus read model to full UI state", () => {
const readModel = projectAgentRuntimeReadModel({
sourceCount: 2,
executionEvents: [
{
id: "evt-artifact",
kind: "draft",
status: "completed",
eventClass: "artifact.changed",
title: "草稿已保存",
artifactRefs: ["artifact-1"],
payload: {
relativePath: "drafts/outline.md",
preview: "文章大纲",
},
sequence: 1,
createdAt: "2026-06-10T00:00:00.000Z",
},
{
id: "evt-evidence",
kind: "evidence",
status: "completed",
eventClass: "evidence.changed",
title: "证据已导出",
evidenceRefs: ["evidence-1"],
payload: {
packRelativeRoot: "evidence/input-source",
summary: "输入源摘要",
},
sequence: 2,
createdAt: "2026-06-10T00:00:01.000Z",
},
],
});
const state = projectAgentUiStateFromSessionSnapshot({
readModel,
messages: [
{
id: "message-user",
role: "user",
content: "请生成小红书标题",
createdAt: "2026-06-10T00:00:02.000Z",
},
{
id: "message-assistant",
role: "assistant",
text: "已生成 5 个标题候选",
refs: ["artifact-1", "evidence-1"],
createdAt: "2026-06-10T00:00:03.000Z",
},
],
});
assert.equal(state.hydration.status, "live");
assert.equal(state.hydration.eventCount, 2);
assert.equal(state.readModel.sourceCount, 2);
assert.deepEqual(
state.messages.map((message) => [message.messageId, message.role, message.text]),
[
["message-user", "user", "请生成小红书标题"],
["message-assistant", "assistant", "已生成 5 个标题候选"],
],
);
assert.deepEqual(state.messages[1].refs, ["artifact-1", "evidence-1"]);
assert.deepEqual(
state.artifacts.map((artifact) => [artifact.id, artifact.path, artifact.preview]),
[["artifact-1", "drafts/outline.md", "文章大纲"]],
);
assert.deepEqual(
state.evidence.map((evidence) => [evidence.id, evidence.path, evidence.preview]),
[["evidence-1", "evidence/input-source", "输入源摘要"]],
);
});
test("createAgentUiProjector applies events idempotently", () => {
const projector = createAgentUiProjector();
const event = {
@@ -564,7 +741,7 @@ test("Agent UI projection summaries classify host-neutral event groups", () => {
);
});
test("Agent UI team workbench summaries group surfaces and lanes", () => {
test("Agent UI subagents summaries group surfaces and lanes", () => {
const events = [
{
type: "team.changed",
@@ -624,9 +801,9 @@ test("Agent UI team workbench summaries group surfaces and lanes", () => {
},
];
const summary = summarizeAgentUiTeamWorkbenchProjectionEvents(events);
const lanes = summarizeAgentUiTeamWorkbenchSurfaceLanes(events);
const surfaces = summarizeAgentUiTeamWorkbenchSurfaces(events, {
const summary = summarizeAgentUiSubagentsProjectionEvents(events);
const lanes = summarizeAgentUiSubagentsSurfaceLanes(events);
const surfaces = summarizeAgentUiSubagentsSurfaces(events, {
latestLimit: 1,
});
+218 -14
View File
@@ -29,9 +29,11 @@ React 是 peer dependency
- 渲染 `UIMessageParts` 消息部件。
- 渲染 `ProcessTimeline` 过程时间线。
- 渲染 `ExecutionGraph` 执行图。
- 渲染 `state.subagents` 标准子代理模型。
- 渲染 Agent 消息时间线。
- 渲染运行事实摘要。
- 渲染 action、evidence、artifact、tool 等 runtime facts。
- 渲染 `ArtifactRef` / `EvidenceRef` 轻量引用列表,并把打开意图交还宿主应用。
- 渲染标准多 action controls,并通过 `data-action-decision` 暴露稳定 DOM contract。
- 通过 callback 把用户处理 action 的意图交还给宿主应用。
@@ -45,6 +47,8 @@ React 是 peer dependency
## Usage
最推荐的入口是 `AgentUiProjectionView`。宿主先用 projection 包得到 `AgentUiProjectionState`,再把 state 传给 UI 包:
```tsx
import { AgentUiProjectionView } from "@limecloud/agent-runtime-ui";
import { projectAgentUiState } from "@limecloud/agent-runtime-projection";
@@ -73,22 +77,90 @@ export function AgentPanel() {
}
```
如果宿主已经通过 App Server facts replay 得到 state,也可以直接渲染:
```tsx
import { AgentUiProjectionView } from "@limecloud/agent-runtime-ui";
import { replayAppServerFacts } from "@limecloud/agent-runtime-projection";
const replay = replayAppServerFacts({
readModel,
events,
evidenceExport,
});
<AgentUiProjectionView
state={replay.state}
onResolveAction={(event, action) => {
actionResponder.respond(event, action);
}}
/>;
```
`onResolveAction` 只表达用户意图。真正的后端调用应回到宿主的 runtime client,例如 `AgentRuntimeClient.respondAction(...)`
## React Surface Model
这个包采用 controlled component 模式:宿主传入 `AgentUiProjectionState` 和 command callbacks,组件只负责渲染、稳定 DOM contract 和用户意图回调。
| Surface | Standard input | Owner of business behavior |
| --- | --- | --- |
| Message parts | `state.messages` | 宿主提供消息列表区域布局和本地化 label。 |
| Process timeline | `state.timeline` | projection 负责排序和 entry kind;宿主负责样式。 |
| Execution graph | `state.graph` | projection 负责 parent/child edge;宿主负责布局密度。 |
| Action required | `state.actions` | 宿主通过 runtime client 提交 action response。 |
| Tool group | `state.tools` | projection 负责 tool lifecycle 解释。 |
| Subagents | `state.subagents` | projection 负责子代理线程、委派调用、活动摘要和隔离摘要。 |
| Artifact refs | `state.artifacts` | 宿主负责打开 artifact workspace 或详情页。 |
| Evidence refs | `state.evidence` | 宿主负责打开 evidence pack、review 或 replay。 |
| Runtime summary | `state.readModel` | projection/read model 负责计数;宿主负责显示文案。 |
当前包不提供 hooks。`useAgentRuntimeClient``useAgentRuntimeFacts``useAgentUiProjector` 等 hook 应留在产品应用或后续单独 adapter 包里;共享 React primitives 不订阅 runtime、不持有 session store、不读取 App Server。
## Components
- `AgentTimeline`
- `AgentUiProjectionView`
- `UIMessagePartsView`
- `ProcessTimelineView`
- `ExecutionGraphView`
- `ToolGroup`
- `ActionRequiredList`
- `RuntimeFactsPanel`
- `RuntimeFactsSummary`
- `RuntimeEventList`
- `RuntimeFactCard`
- `ActionCard`
- `EvidenceCard`
- `ArtifactCard`
| 组件 | 输入 | 用途 |
| --- | --- | --- |
| `AgentUiProjectionView` | `AgentUiProjectionState` | 标准组合入口,渲染消息、过程、事实栏、action 和 graph。 |
| `UIMessagePartsView` | `UIMessageParts` | 渲染标准消息部件。 |
| `ProcessTimelineView` | `ProcessTimeline` | 渲染线性执行过程。 |
| `ExecutionGraphView` | `ExecutionGraph` | 渲染 run / task / tool / subagent 结构。 |
| `SubagentsView` | `AgentUiProjectionState` | 从 `state.subagents` 渲染子代理标准面。 |
| `SubagentThreadList` | `AgentUiSubagentThreadView[]` | 渲染子代理线程、身份、状态、refs 摘要。 |
| `SubagentDelegationList` | `AgentUiSubagentDelegationView[]` | 渲染 spawn / handoff / wait / interrupt 等委派调用。 |
| `SubagentActivityList` | `AgentUiSubagentActivityView[]` | 渲染 started / handoff / review / completed / failed 等活动摘要。 |
| `ArtifactRefList` | `AgentUiArtifactRefView[]` | 渲染 artifact refs,不读取大 payload。 |
| `EvidenceRefList` | `AgentUiEvidenceRefView[]` | 渲染 evidence refs,不读取证据内容。 |
| `AgentUiRefList` | `AgentUiRefView[]` | refs 通用 primitive。 |
| `RuntimeFactsPanel` | `AgentRuntimeReadModel` | 兼容事实栏入口。 |
| `RuntimeFactsSummary` | `AgentRuntimeReadModel` | 渲染 source / action / artifact / evidence 计数。 |
| `RuntimeEventList` | `AgentRuntimeEventProjection[]` | 渲染一般 runtime event 列表。 |
| `ToolGroup` | tool events | 渲染工具调用分组。 |
| `ActionRequiredList` | action events | 渲染待处理 action。 |
| `RuntimeFactCard` | runtime event | 渲染单个 runtime fact。 |
| `ActionCard` / `EvidenceCard` / `ArtifactCard` | runtime event | 语义化 card alias,方便宿主样式分层。 |
| `AgentTimeline` | host message list | 兼容传统消息列表,不是新 projection 默认入口。 |
## Labels And I18n
默认 label 只是 fallback。Lime 产品应用必须传入自己的 i18n label 和 formatter
```tsx
<AgentUiProjectionView
state={state}
labels={{
messagePartsAriaLabel: t("agent.messages.aria"),
processTimelineAriaLabel: t("agent.timeline.aria"),
executionGraphAriaLabel: t("agent.graph.aria"),
runtimeSummaryAriaLabel: t("agent.summary.aria"),
actionRequiredAriaLabel: t("agent.actions.aria"),
actionButtonLabel: (action) => t(`agent.action.${action.decision}`),
eventStatusLabel: (event) => t(`agent.eventStatus.${event.status}`),
}}
/>;
```
宿主需要覆盖 `zh-CN / zh-TW / en-US / ja-JP / ko-KR`,不要把本包 fallback 当成产品文案。
## Source Layout
@@ -100,7 +172,9 @@ src/labels.ts -> 默认 label、status 和 meta formatter
src/messages.tsx -> AgentTimeline / UIMessagePartsView
src/processTimeline.tsx -> ProcessTimelineView
src/executionGraph.tsx -> ExecutionGraphView
src/refs.tsx -> ArtifactRefList / EvidenceRefList / AgentUiRefList
src/runtimeFacts.tsx -> RuntimeFactsPanel / RuntimeFactCard / action/tool lists
src/subagents.tsx -> SubagentsView / SubagentThreadList / SubagentDelegationList / SubagentActivityList
src/projectionView.tsx -> AgentUiProjectionView 标准组合入口
src/index.ts -> barrel exports only
```
@@ -131,6 +205,18 @@ src/index.ts -> barrel exports only
- `agent-execution-events`
- `agent-tool-group`
- `agent-action-required-list`
- `agent-subagents`
- `agent-subagent-threads`
- `agent-subagent-thread`
- `agent-subagent-delegations`
- `agent-subagent-delegation`
- `agent-subagent-activities`
- `agent-subagent-activity`
- `agent-ref-list`
- `agent-ref-card`
- `agent-ref-action`
- `agent-artifact-refs`
- `agent-evidence-refs`
- `agent-execution-graph`
- `agent-execution-node`
- `agent-event-surface`
@@ -143,6 +229,25 @@ src/index.ts -> barrel exports only
这些 class names 是包级 CSS contract。宿主应用可以用自己的设计系统实现样式,不应把样式文件或页面壳反向变成包依赖。
稳定 data attributes
| Attribute | Surface | 用途 |
| --- | --- | --- |
| `data-runtime-status` | `AgentUiProjectionView` | 当前 runtime status。 |
| `data-hydration-status` | `AgentUiProjectionView` | hydration / stale / repair 状态。 |
| `data-action-decision` | action buttons | approve / reject / answer / retry 等 action intent。 |
| `data-node-id` / `data-node-type` / `data-parent-id` | graph | graph node 和 lineage。 |
| `data-subagent-count` / `data-delegation-count` / `data-activity-count` | Subagents | 子代理 smoke / snapshot 断言。 |
| `data-thread-id` / `data-subagent-id` / `data-parent-thread-id` | Subagents | 子代理线程身份和父子关系。 |
| `data-delegation-action` / `data-target-thread-ids` | Subagents | 委派调用类型和目标线程。 |
| `data-activity-kind` / `data-source-event-id` | Subagents | 活动分类和来源事件。 |
| `data-event-class` | runtime fact cards | `tool.*``handoff.*``review.*` 等事件族。 |
| `data-ref-kind` / `data-ref-id` / `data-source-event-id` | artifact / evidence refs | 引用类型、引用 id 和来源事件。 |
## Accessibility
默认 aria label 只是 fallback。产品应用必须通过 `labels` 注入本地化 aria label,并确保可点击 action 使用真实 `<button>`。组件不会把图标、颜色或状态文本作为唯一语义;状态同时通过 class / data attributes 暴露给宿主样式和测试。
## Backend Integration
推荐数据流:
@@ -174,6 +279,85 @@ App Server JSON-RPC
`RuntimeFactsPanel` 只渲染宿主传入的 read model,不会订阅 JSON-RPC,也不会判断某个 artifact 是否已经被业务层物化。宿主如果已经把 App Server `artifact.snapshot` 写成业务产物,应在进入 `@limecloud/agent-runtime-projection` 前去掉上游快照事件,只保留本地业务产物的 `artifact.changed`。这样右侧事实栏展示的是可继续编辑、可应用的交付物,而不是后端中间事件。
## Product App Pattern
产品应用通常这样接入:
```tsx
function ProductAgentPanel({ sessionId }: { sessionId: string }) {
const runtime = useAgentRuntimeClient();
const facts = useAgentRuntimeFacts(sessionId);
const state = projectAgentUiState({
executionEvents: facts.executionEvents,
sourceCount: facts.sourceCount,
});
return (
<AgentUiProjectionView
state={state}
artifact={<ProductArtifactWorkspace sessionId={sessionId} />}
onResolveAction={(event, action) => {
runtime.respondAction({
sessionId,
requestId: event.actionId ?? event.id,
actionType: "ask_user",
confirmed: action.decision !== "reject",
response: action.decision,
});
}}
labels={productAgentLabels}
/>
);
}
```
这段模式里,UI 包只负责渲染和回调。`useAgentRuntimeClient``useAgentRuntimeFacts`、业务 artifact workspace、权限判断和路由都属于宿主。
Subagents 的接入也必须走标准 projection state
```tsx
import { SubagentsView } from "@limecloud/agent-runtime-ui";
<SubagentsView
state={state}
labels={{
subagentsAriaLabel: t("agent.subagents"),
subagentThreadsAriaLabel: t("agent.subagents.threads"),
subagentDelegationsAriaLabel: t("agent.subagents.delegations"),
subagentActivitiesAriaLabel: t("agent.subagents.activities"),
}}
/>;
```
`SubagentsView` 只读取 `state.subagents`。如果业务组件需要自定义子代理布局,也应消费同一个模型,而不是重新过滤 `state.graph``state.readModel.visibleEvents`
## Testing
React surface 测试应使用 contracts fixture 和 projection replay,不要自己编私有状态:
```tsx
import { renderToStaticMarkup } from "react-dom/server";
import { getAgentUiFixture } from "@limecloud/agent-ui-contracts";
import { replayAgentUiFixture } from "@limecloud/agent-runtime-projection";
import { AgentUiProjectionView } from "@limecloud/agent-runtime-ui";
const replay = replayAgentUiFixture(getAgentUiFixture("hitl-action"));
const html = renderToStaticMarkup(
<AgentUiProjectionView state={replay.state} />,
);
expect(html).toContain("data-action-decision");
```
组件测试只覆盖渲染、class / data attribute contract 和 callback 接线。复杂状态机、事件归并和 summary 归类应在 projection 包测试。
标准 fixture smoke 至少覆盖:
- `hitl-action` 渲染 `data-action-decision`
- `tool-success` / `tool-failure` 渲染 ToolGroup。
- `artifact-evidence` 渲染 artifact / evidence refs。
- `subagent-handoff` 渲染 `SubagentsView`、threads、delegation calls、activities。
## Development
```bash
@@ -183,6 +367,26 @@ npm --prefix packages/agent-runtime-ui run test
npm --prefix packages/agent-runtime-ui pack --dry-run
```
## Package Metadata
| Item | Value |
| --- | --- |
| Runtime | Node `>=20`ESM。 |
| Peer deps | `react >=18 <20``react-dom >=18 <20`。 |
| Dependencies | `@limecloud/agent-ui-contracts`。 |
| Dev dependency | `@limecloud/agent-runtime-projection` 仅用于 fixture replay 测试。 |
| Side effects | `false`,不注入全局 CSS。 |
| Public files | `dist``README.md`。 |
| License | `MIT`。 |
## Do Not
- 不要在本包里创建 runtime client、订阅 JSON-RPC、调用 Electron bridge 或访问 App Server。
- 不要在本包里拼 Provider 参数、读取 API key、解释 tool lifecycle 或合成 evidence。
- 不要把业务页面、workspace、Prompt 草稿、素材预览或审核流放进共享 UI 包。
- 不要在 React 组件里重写 projection reducer、scope selector、summary selector 或 runtime facts 解释。
- 不要让默认英文 fallback 进入 Lime 产品文案;产品应用必须注入本地化 label。
## Publish
首次发布到 npmjs
+2
View File
@@ -2,5 +2,7 @@ export type * from "./types.js";
export * from "./messages.js";
export * from "./processTimeline.js";
export * from "./executionGraph.js";
export * from "./refs.js";
export * from "./runtimeFacts.js";
export * from "./subagents.js";
export * from "./projectionView.js";
@@ -3,7 +3,9 @@ import type { AgentRuntimeExecutionEvent } from "@limecloud/agent-ui-contracts";
import { ExecutionGraphView } from "./executionGraph.js";
import { UIMessagePartsView } from "./messages.js";
import { ProcessTimelineView } from "./processTimeline.js";
import { ArtifactRefList, EvidenceRefList } from "./refs.js";
import { ActionRequiredList, RuntimeEventList, RuntimeFactsSummary, ToolGroup } from "./runtimeFacts.js";
import { SubagentsView } from "./subagents.js";
import type { AgentUiProjectionViewProps } from "./types.js";
export function AgentUiProjectionView<TEvent extends AgentRuntimeExecutionEvent = AgentRuntimeExecutionEvent>({
@@ -12,6 +14,9 @@ export function AgentUiProjectionView<TEvent extends AgentRuntimeExecutionEvent
emptyMessages,
labels,
onResolveAction,
onSelectArtifactRef,
onSelectEvidenceRef,
onOpenSubagentThread,
}: AgentUiProjectionViewProps<TEvent>) {
const otherEvents = state.readModel.visibleEvents.filter(
(event) => event.surface !== "human-action" && event.surface !== "tool",
@@ -64,6 +69,29 @@ export function AgentUiProjectionView<TEvent extends AgentRuntimeExecutionEvent
actionButtonLabel={labels?.actionButtonLabel}
eventStatusLabel={labels?.eventStatusLabel}
/>
<ArtifactRefList
refs={state.artifacts}
ariaLabel={labels?.artifactRefsAriaLabel}
refTitle={labels?.artifactRefTitle}
refMeta={labels?.artifactRefMeta}
refPreview={labels?.artifactRefPreview}
refActionLabel={labels?.artifactRefActionLabel}
onSelectRef={onSelectArtifactRef}
/>
<EvidenceRefList
refs={state.evidence}
ariaLabel={labels?.evidenceRefsAriaLabel}
refTitle={labels?.evidenceRefTitle}
refMeta={labels?.evidenceRefMeta}
refPreview={labels?.evidenceRefPreview}
refActionLabel={labels?.evidenceRefActionLabel}
onSelectRef={onSelectEvidenceRef}
/>
<SubagentsView
state={state}
labels={labels}
onOpenThread={onOpenSubagentThread}
/>
{artifact ? <div className="agent-session-artifact">{artifact}</div> : null}
<ExecutionGraphView
nodes={state.graph}
+100
View File
@@ -0,0 +1,100 @@
import type { ReactNode } from "react";
import type {
AgentUiArtifactRefView,
AgentUiEvidenceRefView,
AgentUiRefView,
} from "@limecloud/agent-ui-contracts";
import type {
AgentUiRefListProps,
ArtifactRefListProps,
EvidenceRefListProps,
} from "./types.js";
function defaultRefTitle(ref: AgentUiRefView): ReactNode {
return ref.title ?? ref.id;
}
function defaultRefMeta(ref: AgentUiRefView): ReactNode {
return ref.path ?? ref.contentRef ?? ref.sourceEventId;
}
function defaultRefPreview(ref: AgentUiRefView): ReactNode {
return ref.preview;
}
export function AgentUiRefList<TRef extends AgentUiRefView = AgentUiRefView>({
refs = [],
empty,
ariaLabel = "Runtime refs",
className = "agent-ref-list",
refKind = "ref",
refTitle = defaultRefTitle,
refMeta = defaultRefMeta,
refPreview = defaultRefPreview,
refActionLabel,
onSelectRef,
}: AgentUiRefListProps<TRef>) {
if (!refs.length) return empty === undefined ? null : <div className={`${className}-empty`}>{empty}</div>;
return (
<div className={className} aria-label={ariaLabel} data-ref-kind={refKind}>
{refs.map((ref) => (
<article
key={ref.id}
className="agent-ref-card"
data-ref-kind={refKind}
data-ref-id={ref.id}
data-source-event-id={ref.sourceEventId}
data-ref-status={ref.status}
data-content-ref={ref.contentRef}
>
<div>
<small>{refKind}</small>
<strong>{refTitle(ref)}</strong>
<em>{refMeta(ref)}</em>
</div>
{refPreview ? <p>{refPreview(ref)}</p> : null}
{onSelectRef ? (
<button type="button" className="agent-ref-action" onClick={() => onSelectRef(ref)}>
{refActionLabel ? refActionLabel(ref) : "Open"}
</button>
) : null}
</article>
))}
</div>
);
}
export function ArtifactRefList({
ariaLabel = "Artifact refs",
className = "agent-artifact-refs",
refKind = "artifact",
...props
}: ArtifactRefListProps) {
return (
<AgentUiRefList<AgentUiArtifactRefView>
{...props}
ariaLabel={ariaLabel}
className={className}
refKind={refKind}
/>
);
}
export function EvidenceRefList({
ariaLabel = "Evidence refs",
className = "agent-evidence-refs",
refKind = "evidence",
...props
}: EvidenceRefListProps) {
return (
<AgentUiRefList<AgentUiEvidenceRefView>
{...props}
ariaLabel={ariaLabel}
className={className}
refKind={refKind}
/>
);
}
@@ -72,7 +72,7 @@ export function RuntimeFactCard<TEvent extends AgentRuntimeExecutionEvent = Agen
: [];
return (
<article
className={event.status}
className={`agent-runtime-event ${event.status}`}
data-event-id={event.id}
data-event-class={event.source.eventClass}
data-owner={event.source.owner}
+187
View File
@@ -0,0 +1,187 @@
import type {
AgentRuntimeExecutionEvent,
AgentUiSubagentActivityView,
AgentUiSubagentDelegationView,
AgentUiSubagentThreadView,
} from "@limecloud/agent-ui-contracts";
import type {
SubagentActivityListProps,
SubagentDelegationListProps,
SubagentsViewProps,
SubagentThreadListProps,
} from "./types.js";
function defaultThreadTitle(thread: AgentUiSubagentThreadView) {
return thread.nickname ?? thread.role ?? thread.title;
}
function defaultThreadMeta(thread: AgentUiSubagentThreadView) {
return [thread.status, thread.taskPath ?? thread.threadId]
.filter(Boolean)
.join(" / ");
}
function defaultThreadSummary(thread: AgentUiSubagentThreadView) {
return thread.summary ?? thread.promptPreview;
}
function defaultDelegationTitle(delegation: AgentUiSubagentDelegationView) {
return `${delegation.action}: ${delegation.title}`;
}
function defaultActivityTitle(activity: AgentUiSubagentActivityView) {
return activity.title;
}
function defaultActivityMeta(activity: AgentUiSubagentActivityView) {
return `${activity.kind} / ${activity.status}`;
}
export function SubagentThreadList({
threads = [],
empty,
ariaLabel = "Subagent threads",
threadTitle = defaultThreadTitle,
threadMeta = defaultThreadMeta,
threadSummary = defaultThreadSummary,
threadActionLabel,
onOpenThread,
}: SubagentThreadListProps) {
if (!threads.length) {
return empty === undefined ? null : <div className="agent-subagent-threads-empty">{empty}</div>;
}
return (
<div className="agent-subagent-threads" aria-label={ariaLabel}>
{threads.map((thread) => (
<article
key={thread.threadId}
className={`agent-subagent-thread ${thread.status}`}
data-thread-id={thread.threadId}
data-subagent-id={thread.subagentId}
data-parent-thread-id={thread.parentThreadId}
data-task-id={thread.taskId}
data-subagent-status={thread.status}
>
<div>
<small>{threadMeta(thread)}</small>
<strong>{threadTitle(thread)}</strong>
{threadSummary(thread) ? <p>{threadSummary(thread)}</p> : null}
</div>
{onOpenThread ? (
<button type="button" onClick={() => onOpenThread(thread)}>
{threadActionLabel ? threadActionLabel(thread) : "Open"}
</button>
) : null}
</article>
))}
</div>
);
}
export function SubagentDelegationList({
delegations = [],
empty,
ariaLabel = "Subagent delegations",
delegationTitle = defaultDelegationTitle,
}: SubagentDelegationListProps) {
if (!delegations.length) {
return empty === undefined ? null : <div className="agent-subagent-delegations-empty">{empty}</div>;
}
return (
<div className="agent-subagent-delegations" aria-label={ariaLabel}>
{delegations.map((delegation) => (
<article
key={delegation.callId}
className={`agent-subagent-delegation ${delegation.status}`}
data-delegation-call-id={delegation.callId}
data-delegation-action={delegation.action}
data-parent-thread-id={delegation.parentThreadId}
data-target-thread-ids={delegation.targetThreadIds.join(" ")}
>
<small>{delegation.status}</small>
<strong>{delegationTitle(delegation)}</strong>
{delegation.promptPreview ? <p>{delegation.promptPreview}</p> : null}
</article>
))}
</div>
);
}
export function SubagentActivityList({
activities = [],
empty,
ariaLabel = "Subagent activities",
activityTitle = defaultActivityTitle,
activityMeta = defaultActivityMeta,
}: SubagentActivityListProps) {
if (!activities.length) {
return empty === undefined ? null : <div className="agent-subagent-activities-empty">{empty}</div>;
}
return (
<div className="agent-subagent-activities" aria-label={ariaLabel}>
{activities.map((activity) => (
<article
key={activity.activityId}
className={`agent-subagent-activity ${activity.status}`}
data-activity-id={activity.activityId}
data-thread-id={activity.threadId}
data-activity-kind={activity.kind}
data-source-event-id={activity.sourceEventId}
>
<small>{activityMeta(activity)}</small>
<strong>{activityTitle(activity)}</strong>
</article>
))}
</div>
);
}
export function SubagentsView<TEvent extends AgentRuntimeExecutionEvent = AgentRuntimeExecutionEvent>({
state,
emptyThreads,
emptyDelegations,
emptyActivities,
labels,
onOpenThread,
}: SubagentsViewProps<TEvent>) {
const model = state.subagents;
if (!model.hasSubagents) {
return null;
}
return (
<section
className="agent-subagents"
aria-label={labels?.subagentsAriaLabel ?? "Subagents"}
data-subagent-count={model.threads.length}
data-delegation-count={model.delegationCalls.length}
data-activity-count={model.activities.length}
data-active-subagent-count={model.activeThreadIds.length}
>
<SubagentThreadList
threads={model.threads}
empty={emptyThreads}
ariaLabel={labels?.subagentThreadsAriaLabel}
threadTitle={labels?.subagentThreadTitle}
threadMeta={labels?.subagentThreadMeta}
threadSummary={labels?.subagentThreadSummary}
onOpenThread={onOpenThread}
/>
<SubagentDelegationList
delegations={model.delegationCalls}
empty={emptyDelegations}
ariaLabel={labels?.subagentDelegationsAriaLabel}
delegationTitle={labels?.subagentDelegationTitle}
/>
<SubagentActivityList
activities={model.activities}
empty={emptyActivities}
ariaLabel={labels?.subagentActivitiesAriaLabel}
activityTitle={labels?.subagentActivityTitle}
activityMeta={labels?.subagentActivityMeta}
/>
</section>
);
}
+93
View File
@@ -2,6 +2,12 @@ import type { ReactNode } from "react";
import type {
AgentUiProjectionState,
AgentUiArtifactRefView,
AgentUiEvidenceRefView,
AgentUiRefView,
AgentUiSubagentActivityView,
AgentUiSubagentDelegationView,
AgentUiSubagentThreadView,
AgentRuntimeActionProjection,
AgentRuntimeEventProjection,
AgentRuntimeExecutionEvent,
@@ -116,10 +122,33 @@ export interface ExecutionGraphViewProps {
nodeMeta?: (node: ExecutionGraphNode) => ReactNode;
}
export interface AgentUiRefListProps<TRef extends AgentUiRefView = AgentUiRefView> {
refs?: readonly TRef[];
empty?: ReactNode;
ariaLabel?: string;
className?: string;
refKind?: string;
refTitle?: (ref: TRef) => ReactNode;
refMeta?: (ref: TRef) => ReactNode;
refPreview?: (ref: TRef) => ReactNode;
refActionLabel?: (ref: TRef) => ReactNode;
onSelectRef?: (ref: TRef) => void;
}
export type ArtifactRefListProps = AgentUiRefListProps<AgentUiArtifactRefView>;
export type EvidenceRefListProps = AgentUiRefListProps<AgentUiEvidenceRefView>;
export interface AgentUiProjectionViewLabels<TEvent extends AgentRuntimeExecutionEvent = AgentRuntimeExecutionEvent> {
messagePartsAriaLabel?: string;
processTimelineAriaLabel?: string;
executionGraphAriaLabel?: string;
artifactRefsAriaLabel?: string;
evidenceRefsAriaLabel?: string;
subagentsAriaLabel?: string;
subagentThreadsAriaLabel?: string;
subagentDelegationsAriaLabel?: string;
subagentActivitiesAriaLabel?: string;
runtimeSummaryAriaLabel?: string;
executionEventsAriaLabel?: string;
toolGroupAriaLabel?: string;
@@ -133,6 +162,20 @@ export interface AgentUiProjectionViewLabels<TEvent extends AgentRuntimeExecutio
timelineEntryMeta?: ProcessTimelineViewProps["entryMeta"];
graphNodeTitle?: ExecutionGraphViewProps["nodeTitle"];
graphNodeMeta?: ExecutionGraphViewProps["nodeMeta"];
artifactRefTitle?: ArtifactRefListProps["refTitle"];
artifactRefMeta?: ArtifactRefListProps["refMeta"];
artifactRefPreview?: ArtifactRefListProps["refPreview"];
artifactRefActionLabel?: ArtifactRefListProps["refActionLabel"];
evidenceRefTitle?: EvidenceRefListProps["refTitle"];
evidenceRefMeta?: EvidenceRefListProps["refMeta"];
evidenceRefPreview?: EvidenceRefListProps["refPreview"];
evidenceRefActionLabel?: EvidenceRefListProps["refActionLabel"];
subagentThreadTitle?: SubagentThreadListProps["threadTitle"];
subagentThreadMeta?: SubagentThreadListProps["threadMeta"];
subagentThreadSummary?: SubagentThreadListProps["threadSummary"];
subagentDelegationTitle?: SubagentDelegationListProps["delegationTitle"];
subagentActivityTitle?: SubagentActivityListProps["activityTitle"];
subagentActivityMeta?: SubagentActivityListProps["activityMeta"];
actionButtonLabel?: RuntimeFactCardProps<TEvent>["actionButtonLabel"];
eventStatusLabel?: RuntimeFactCardProps<TEvent>["eventStatusLabel"];
}
@@ -143,4 +186,54 @@ export interface AgentUiProjectionViewProps<TEvent extends AgentRuntimeExecution
emptyMessages?: ReactNode;
labels?: AgentUiProjectionViewLabels<TEvent>;
onResolveAction?: AgentRuntimeActionResolver<TEvent>;
onSelectArtifactRef?: ArtifactRefListProps["onSelectRef"];
onSelectEvidenceRef?: EvidenceRefListProps["onSelectRef"];
onOpenSubagentThread?: SubagentsViewProps["onOpenThread"];
}
export interface SubagentThreadListProps {
threads?: readonly AgentUiSubagentThreadView[];
empty?: ReactNode;
ariaLabel?: string;
threadTitle?: (thread: AgentUiSubagentThreadView) => ReactNode;
threadMeta?: (thread: AgentUiSubagentThreadView) => ReactNode;
threadSummary?: (thread: AgentUiSubagentThreadView) => ReactNode;
threadActionLabel?: (thread: AgentUiSubagentThreadView) => ReactNode;
onOpenThread?: (thread: AgentUiSubagentThreadView) => void;
}
export interface SubagentDelegationListProps {
delegations?: readonly AgentUiSubagentDelegationView[];
empty?: ReactNode;
ariaLabel?: string;
delegationTitle?: (delegation: AgentUiSubagentDelegationView) => ReactNode;
}
export interface SubagentActivityListProps {
activities?: readonly AgentUiSubagentActivityView[];
empty?: ReactNode;
ariaLabel?: string;
activityTitle?: (activity: AgentUiSubagentActivityView) => ReactNode;
activityMeta?: (activity: AgentUiSubagentActivityView) => ReactNode;
}
export interface SubagentsViewProps<TEvent extends AgentRuntimeExecutionEvent = AgentRuntimeExecutionEvent> {
state: AgentUiProjectionState<TEvent>;
emptyThreads?: ReactNode;
emptyDelegations?: ReactNode;
emptyActivities?: ReactNode;
onOpenThread?: (thread: AgentUiSubagentThreadView) => void;
labels?: Pick<
AgentUiProjectionViewLabels<TEvent>,
| "subagentsAriaLabel"
| "subagentThreadsAriaLabel"
| "subagentDelegationsAriaLabel"
| "subagentActivitiesAriaLabel"
| "subagentThreadTitle"
| "subagentThreadMeta"
| "subagentThreadSummary"
| "subagentDelegationTitle"
| "subagentActivityTitle"
| "subagentActivityMeta"
>;
}
+131 -2
View File
@@ -3,9 +3,23 @@ import { renderToStaticMarkup } from "react-dom/server";
import test from "node:test";
import assert from "node:assert/strict";
import { projectAgentUiState } from "@limecloud/agent-runtime-projection";
import {
agentUiConformanceFixtures,
getAgentUiFixture,
} from "@limecloud/agent-ui-contracts";
import {
projectAgentUiState,
replayAgentUiFixture,
} from "@limecloud/agent-runtime-projection";
import { AgentTimeline, AgentUiProjectionView, RuntimeFactsPanel } from "../dist/index.js";
import {
AgentTimeline,
AgentUiProjectionView,
ArtifactRefList,
EvidenceRefList,
RuntimeFactsPanel,
SubagentsView,
} from "../dist/index.js";
test("AgentTimeline renders user and assistant messages", () => {
const markup = renderToStaticMarkup(
@@ -78,6 +92,7 @@ test("RuntimeFactsPanel renders action button and fact counts", () => {
const markup = renderToStaticMarkup(React.createElement(RuntimeFactsPanel, { readModel, onResolveAction: () => {} }));
assert.match(markup, /Runtime facts summary/);
assert.match(markup, /agent-runtime-event pending/);
assert.match(markup, /Input sources/);
assert.match(markup, /Actions/);
assert.match(markup, /需要补充输入源/);
@@ -269,3 +284,117 @@ test("AgentUiProjectionView accepts host-provided labels", () => {
assert.match(markup, /打开模型设置/);
assert.match(markup, /执行图/);
});
test("AgentUiProjectionView renders standard fixture replay states", () => {
for (const fixture of agentUiConformanceFixtures) {
const replay = replayAgentUiFixture(fixture);
const markup = renderToStaticMarkup(
React.createElement(AgentUiProjectionView, {
state: replay.state,
onResolveAction: () => {},
}),
);
assert.equal(replay.passed, true, fixture.id);
assert.match(markup, /agent-ui-projection/, fixture.id);
assert.match(markup, /data-hydration-status="live"/, fixture.id);
}
});
test("AgentUiProjectionView renders subagent handoff fixture graph", () => {
const replay = replayAgentUiFixture(getAgentUiFixture("subagent-handoff"));
const markup = renderToStaticMarkup(
React.createElement(AgentUiProjectionView, {
state: replay.state,
onResolveAction: () => {},
}),
);
assert.match(markup, /data-node-type="subagent"/);
assert.match(markup, /data-parent-id="task_fixture"/);
assert.match(markup, /agent-subagents/);
assert.match(markup, /Subagent threads/);
assert.match(markup, /Subagent delegations/);
assert.match(markup, /Subagent activities/);
assert.match(markup, /data-subagent-id="subagent_fixture_researcher"/);
assert.match(markup, /data-delegation-action="handoff"/);
assert.match(markup, /Research subagent started/);
assert.match(markup, /data-source-event-id="evt_handoff_requested"/);
});
test("AgentUiProjectionView renders artifact and evidence refs from projection state", () => {
const replay = replayAgentUiFixture(getAgentUiFixture("artifact-evidence"));
const markup = renderToStaticMarkup(
React.createElement(AgentUiProjectionView, {
state: replay.state,
labels: {
artifactRefsAriaLabel: "交付物引用",
evidenceRefsAriaLabel: "证据引用",
artifactRefActionLabel: () => "打开交付物",
evidenceRefActionLabel: () => "打开证据",
},
onSelectArtifactRef: () => {},
onSelectEvidenceRef: () => {},
}),
);
assert.match(markup, /交付物引用/);
assert.match(markup, /证据引用/);
assert.match(markup, /data-ref-kind="artifact"/);
assert.match(markup, /data-ref-kind="evidence"/);
assert.match(markup, /data-ref-id="artifact_fixture_1"/);
assert.match(markup, /data-ref-id="evidence_fixture_1"/);
assert.match(markup, /打开交付物/);
assert.match(markup, /打开证据/);
});
test("ArtifactRefList and EvidenceRefList expose stable DOM contracts", () => {
const markup = renderToStaticMarkup(
React.createElement(
React.Fragment,
null,
React.createElement(ArtifactRefList, {
refs: [{ id: "artifact-1", sourceEventId: "evt-artifact" }],
refTitle: (ref) => `Artifact ${ref.id}`,
}),
React.createElement(EvidenceRefList, {
refs: [{ id: "evidence-1", sourceEventId: "evt-evidence" }],
refTitle: (ref) => `Evidence ${ref.id}`,
}),
),
);
assert.match(markup, /agent-artifact-refs/);
assert.match(markup, /agent-evidence-refs/);
assert.match(markup, /data-source-event-id="evt-artifact"/);
assert.match(markup, /data-source-event-id="evt-evidence"/);
assert.match(markup, /Artifact artifact-1/);
assert.match(markup, /Evidence evidence-1/);
});
test("SubagentsView renders threads, delegations, and activities from projection state", () => {
const replay = replayAgentUiFixture(getAgentUiFixture("subagent-handoff"));
const markup = renderToStaticMarkup(
React.createElement(SubagentsView, {
state: replay.state,
labels: {
subagentsAriaLabel: "子代理",
subagentThreadsAriaLabel: "子代理线程",
subagentDelegationsAriaLabel: "委派调用",
subagentActivitiesAriaLabel: "活动记录",
},
}),
);
assert.match(markup, /子代理/);
assert.match(markup, /子代理线程/);
assert.match(markup, /委派调用/);
assert.match(markup, /活动记录/);
assert.match(markup, /data-subagent-count="1"/);
assert.match(markup, /data-delegation-count="2"/);
assert.match(markup, /data-activity-count="4"/);
assert.match(markup, /data-thread-id="subagent_fixture_researcher"/);
assert.match(markup, /data-delegation-action="spawn"/);
assert.match(markup, /data-delegation-action="handoff"/);
assert.match(markup, /data-activity-kind="handoff"/);
});
+221 -4
View File
@@ -1,6 +1,6 @@
# @limecloud/agent-ui-contracts
`@limecloud/agent-ui-contracts` 是 Lime Agent UI / Runtime 共享的类型契约事实源。它导出 TypeScript 类型,不包含投影逻辑、React 组件、JSON-RPC client 或 Electron bridge。
`@limecloud/agent-ui-contracts` 是 Lime Agent UI / Runtime 共享的契约事实源。它导出 TypeScript 类型、标准 conformance fixtures 和轻量 validation helpers,不包含投影逻辑、React 组件、JSON-RPC client 或 Electron bridge。
## Boundary
@@ -8,8 +8,10 @@
- runtime execution event 最小公共形状。
- 跨宿主 Agent UI adapter event 最小公共形状。
- action、read model、message parts、process timeline、execution graph 和 projection state 类型。
- action、read model、message parts、process timeline、execution graph、Subagents 和 projection state 类型。
- `AgentUiProjector` 等纯接口定义。
- 标准 conformance fixturestext、tool、HITL、artifact/evidence、stream repair、subagent handoff。
- validation helpersruntime event、read model、projection state、fixture 的最小结构校验。
这个包不负责:
@@ -31,6 +33,16 @@
-> React primitives for the projection state
```
当前物理包名以 `@limecloud/agent-runtime-projection``@limecloud/agent-runtime-ui` 为准。`@limecloud/agent-ui-projection``@limecloud/agent-ui-react` 只允许作为历史规划名或未来 alias 讨论,不能写成 current owner。
## Install
```bash
npm install @limecloud/agent-ui-contracts
```
这个包通常被另外三个包直接依赖。产品应用只有在需要声明自己的 adapter input / output 类型、编写 conformance fixture,或在边界处做最小结构校验时才直接导入它。
## Source Layout
实现必须按职责拆分,`src/index.ts` 只能做 type-only barrel exports
@@ -38,19 +50,224 @@
```text
src/events.ts -> Agent UI adapter event taxonomy
src/runtime.ts -> runtime execution event / read model / action contracts
src/projection.ts -> AgentUiProjectionState / projector contracts
src/projection.ts -> AgentUiProjectionState / Subagents / projector contracts
src/messages.ts -> UI message part contracts
src/timeline.ts -> process timeline contracts
src/graph.ts -> execution graph contracts
src/index.ts -> barrel exports only
src/fixtures.ts -> standard conformance fixtures
src/validation.ts -> minimal contract validation helpers
src/index.ts -> barrel exports
```
新增类型必须落在对应职责文件;不得把事件、runtime、projection、message、timeline 和 graph 类型重新合并回 `src/index.ts`
`AgentRuntimeEventProjection.action` 保留兼容单按钮读取,`AgentRuntimeEventProjection.actions` 是标准多 action controls 表达,用于 approve / reject / answer / retry / stop 等宿主 intent。
## Quick Start
定义运行时事件时,只声明结构化事实,不写 UI 文案推断和 provider 原始对象:
```ts
import type { AgentRuntimeExecutionEvent } from "@limecloud/agent-ui-contracts";
export function toolStartedEvent(params: {
turnId: string;
toolCallId: string;
toolName: string;
}): AgentRuntimeExecutionEvent {
return {
id: `tool:${params.toolCallId}:started`,
schemaVersion: "lime-runtime-event/v0.1",
runtimeId: "runtime-1",
threadId: "thread-1",
turnId: params.turnId,
sequence: 1,
kind: "tool",
status: "running",
eventClass: "tool.started",
title: params.toolName,
toolCallId: params.toolCallId,
runId: params.turnId,
createdAt: new Date().toISOString(),
payload: {
toolName: params.toolName,
},
};
}
```
定义 UI projection state 类型时只引用 contracts,不从 React 或 runtime client 反向导入:
```ts
import type {
AgentUiProjectionState,
AgentUiSubagentsModel,
ProcessTimeline,
UIMessageParts,
} from "@limecloud/agent-ui-contracts";
export interface HostAgentPanelState {
messages: UIMessageParts;
timeline: ProcessTimeline;
projection: AgentUiProjectionState;
subagents: AgentUiSubagentsModel;
}
```
`AgentUiProjectionState.subagents` 是标准子代理模型。React 组件和产品应用不能再从 `graph``readModel.visibleEvents`、assistant 正文或本地 state 重新解释 subagent、handoff、review 事实;这些解释必须在 projection 层完成。
## Core Event Taxonomy
Contracts 层只定义事实 envelope 和稳定枚举,不规定传输方式。事件族按 `eventClass` 划分:
| Event family | Required scope | Standard UI surface |
| --- | --- | --- |
| `turn.*` / `run.*` | `threadId``turnId``runId` | runtime status、ProcessTimeline。 |
| `model.*` / `reasoning.*` | message id 或 turn scope | `UIMessageParts`。 |
| `tool.*` | `toolCallId` | ToolGroup、ProcessTimeline、ExecutionGraph。 |
| `action.*` | `actionId` | ActionRequired。 |
| `artifact.*` | `artifactId``artifactRefs` | ArtifactRef / artifact workspace。 |
| `evidence.*` / `review.*` | `evidenceId``evidenceRefs` | EvidenceRef、review lane。 |
| `task.*` / `subagent.*` / `handoff.*` | `taskId``subagentId``handoffId` | ExecutionGraph、Subagents。 |
| `snapshot.*` / `stream.*` | sequence / cursor | hydration、repair diagnostics。 |
新增事件族时先补 contracts fixture 和 validation 文档,再让 projection / UI 包消费;不要先在产品组件里私有解释。
## Fixtures
`agentUiConformanceFixtures` 固定当前最小验收切片:
- `text-basic`
- `tool-success`
- `tool-failure`
- `hitl-action`
- `artifact-evidence`
- `stream-repair`
- `subagent-handoff`
这些 fixtures 只包含 normalized runtime facts 和 read model 摘要,不包含 React props、Provider SDK 对象或 App Server transport。
宿主或下游包新增投影能力时,优先扩展 fixture,再让 projection / UI 测试消费同一份 fixture
```ts
import {
agentUiConformanceFixtures,
getAgentUiFixture,
} from "@limecloud/agent-ui-contracts";
for (const fixture of agentUiConformanceFixtures) {
console.log(fixture.id, fixture.events.length);
}
const hitl = getAgentUiFixture("hitl-action");
console.log(hitl.expected.pendingActionCount);
```
新增 fixture 的最低要求:
- `id` 稳定,能作为测试名和文档锚点。
- `schemaVersion` 使用当前 `AGENT_UI_FIXTURE_SCHEMA_VERSION`
- `events` 只包含 normalized runtime facts。
- `readModel` 只放结构化摘要,不放 React props。
- `expected` 描述 UI 应见事实,不描述具体 DOM 样式。
## Validation
```ts
import {
validateAgentUiFixture,
validateRuntimeEvent,
} from "@limecloud/agent-ui-contracts";
validateRuntimeEvent(event);
validateAgentUiFixture(fixture);
```
Validation helpers 只做合同层最小检查:必需字段、scope id、sequence gap、secret-bearing key 和大 payload inline 风险。复杂 reducer 行为必须放在 projection 包验证。
`validateProjectionState` 要求 projection state 包含完整 `subagents` 模型:
```ts
{
hasSubagents: boolean;
threads: AgentUiSubagentThreadView[];
delegationCalls: AgentUiSubagentDelegationView[];
activities: AgentUiSubagentActivityView[];
activeThreadIds: string[];
completedThreadIds: string[];
failedThreadIds: string[];
}
```
缺少这些字段时,projection state 不符合 Lime AgentUI 标准。
边界层建议使用 `collect*ValidationIssues` 做非抛错检查,把问题汇入宿主诊断:
```ts
import { collectRuntimeEventValidationIssues } from "@limecloud/agent-ui-contracts";
const issues = collectRuntimeEventValidationIssues(event);
if (issues.length > 0) {
reportContractIssues(issues);
}
```
## Product App Usage
产品应用直接使用本包时,通常只做三件事:
1. 为自己的 adapter 参数声明标准 event / projection 类型。
2. 在接入测试中读取 `agentUiConformanceFixtures`
3. 在 App Server facts 进入 projection 前做最小合同校验。
推荐接入边界:
```text
Product App runtime service
-> AgentRuntimeExecutionEvent[]
-> validateRuntimeEvent / validateAgentUiFixture
-> @limecloud/agent-runtime-projection
-> @limecloud/agent-runtime-ui 或产品自己的 presentation adapter
```
## Conformance
一个 runtime provider 或产品 adapter 声称兼容 Lime AgentUI contracts 时,至少要证明:
| Slice | Required proof |
| --- | --- |
| Text turn | `text-basic` fixture validation 通过,streaming text 可 replay。 |
| Tool lifecycle | `tool-success` / `tool-failure``toolCallId`,大输出走 refs。 |
| HITL | `hitl-action``action.required` / `action.resolved``actionId`。 |
| Artifact / evidence | refs 不内联大 payload,不泄露 secret-bearing key。 |
| Stream repair | sequence gap 只在声明 repair diagnostics 的 fixture 中允许。 |
| Subagents | `subagent-handoff` 能表达 task、subagent、handoff、review scope。 |
| Subagents | projection state 包含完整 `subagents` 字段。 |
下游包应从 `agentUiConformanceFixtures` 读取标准样本,而不是在自己的测试里复制一套私有 fixture。
## Package Metadata
| Item | Value |
| --- | --- |
| Runtime | Node `>=20`ESM。 |
| Side effects | `false`,只导出类型、fixtures 和 validation helpers。 |
| Public files | `dist``README.md`。 |
| License | `MIT`。 |
| Versioning | 与 AgentUI 四包同步发布;contracts 破坏性变更必须走 major。 |
## Do Not
- 不要在本包里实现 reducer、selector、React 组件或 JSON-RPC client。
- 不要把 Provider SDK 类型、API key、HTTP response 或 Electron bridge 类型放进 contracts。
- 不要把业务 App 的私有 task、prompt、artifact schema 当成标准 contracts。
- 不要恢复旧树形过程术语作为标准;过程 UI 标准是 `UIMessageParts``ProcessTimeline``ExecutionGraph`
- 不要在 `src/index.ts` 写实现逻辑;它只能做 barrel exports。
## Development
```bash
npm --prefix packages/agent-ui-contracts run build
npm --prefix packages/agent-ui-contracts run test
npm --prefix packages/agent-ui-contracts pack --dry-run
```
+454
View File
@@ -0,0 +1,454 @@
import type {
AgentRuntimeExecutionEvent,
AgentRuntimeReadModel,
} from "./runtime";
export const AGENT_UI_FIXTURE_SCHEMA_VERSION = "agent-ui-fixture/v0.1";
export interface AgentUiFixtureExpectation {
status: string;
messagePartCount?: number;
timelineEntryCount?: number;
graphNodeCount?: number;
pendingActionCount?: number;
artifactCount?: number;
evidenceCount?: number;
subagents?: {
hasSubagents?: boolean;
threadCount?: number;
delegationCallCount?: number;
activityCount?: number;
activeThreadCount?: number;
completedThreadCount?: number;
failedThreadCount?: number;
};
diagnostics?: string[];
}
export interface AgentUiFixture<
TEvent extends AgentRuntimeExecutionEvent = AgentRuntimeExecutionEvent,
> {
id: string;
schemaVersion: string;
title: string;
events: TEvent[];
initialReadModel?: AgentRuntimeReadModel<TEvent>;
finalReadModel?: AgentRuntimeReadModel<TEvent>;
expected: AgentUiFixtureExpectation;
}
const runtimeId = "agent_ui_fixture_runtime";
const threadId = "thread_fixture";
const turnId = "turn_fixture";
const taskId = "task_fixture";
const createdAt = "2026-06-10T00:00:00.000Z";
function event(
input: Omit<
AgentRuntimeExecutionEvent,
"createdAt" | "id" | "runtimeId" | "schemaVersion" | "threadId" | "turnId"
> & { id: string },
): AgentRuntimeExecutionEvent {
return {
schemaVersion: "lime-runtime-event/v0.1",
runtimeId,
threadId,
turnId,
createdAt,
...input,
};
}
function readModel(
events: AgentRuntimeExecutionEvent[],
overrides: Partial<AgentRuntimeReadModel> = {},
): AgentRuntimeReadModel {
return {
events: events.map((source) => projection(source)),
visibleEvents: events.map((source) => projection(source)),
pendingActions: [],
inputSourceRecovery: false,
sourceCount: 0,
artifactRefs: events.flatMap((source) => source.artifactRefs ?? []),
evidenceRefs: events.flatMap((source) => source.evidenceRefs ?? []),
taskRefs: [taskId],
...overrides,
};
}
function projection(source: AgentRuntimeExecutionEvent) {
return {
id: `projection_${source.id}`,
source,
surface: "runtime-status" as const,
title: source.title,
detail: source.detail,
status: source.status,
displayStatusKey: `agent.status.${source.status}`,
resolved: source.status !== "blocked" && source.status !== "pending",
actionKind: source.actionId ? "runtime_action" : "none",
targetModule: "agent-runtime",
actionId: source.actionId,
};
}
const textBasicEvents = [
event({
id: "evt_text_submitted",
eventClass: "turn.submitted",
kind: "state",
status: "pending",
sequence: 1,
title: "Turn submitted",
}),
event({
id: "evt_text_started",
eventClass: "turn.started",
kind: "state",
status: "running",
sequence: 2,
title: "Turn started",
}),
event({
id: "evt_text_delta",
eventClass: "model.delta",
kind: "model",
status: "running",
sequence: 3,
title: "Assistant text",
payload: { delta: "你好,Lime。" },
}),
event({
id: "evt_text_completed",
eventClass: "model.completed",
kind: "model",
status: "completed",
sequence: 4,
title: "Assistant final text",
payload: { text: "你好,Lime。" },
}),
event({
id: "evt_text_turn_completed",
eventClass: "turn.completed",
kind: "state",
status: "completed",
sequence: 5,
title: "Turn completed",
completedAt: createdAt,
}),
event({
id: "evt_text_snapshot",
eventClass: "snapshot.updated",
kind: "state",
status: "completed",
sequence: 6,
title: "Snapshot updated",
}),
];
const toolSuccessEvents = [
event({
id: "evt_tool_started",
eventClass: "tool.started",
kind: "tool",
status: "running",
sequence: 1,
taskId,
toolCallId: "tool_fixture_search",
title: "Search started",
payload: { toolName: "search" },
}),
event({
id: "evt_tool_result",
eventClass: "tool.result",
kind: "tool",
status: "completed",
sequence: 2,
taskId,
toolCallId: "tool_fixture_search",
title: "Search completed",
refIds: ["output_search_1"],
}),
event({
id: "evt_tool_turn_completed",
eventClass: "turn.completed",
kind: "state",
status: "completed",
sequence: 3,
taskId,
title: "Turn completed",
}),
];
const toolFailureEvents = [
event({
id: "evt_tool_failed_start",
eventClass: "tool.started",
kind: "tool",
status: "running",
sequence: 1,
taskId,
toolCallId: "tool_fixture_write",
title: "Write file started",
}),
event({
id: "evt_tool_failed",
eventClass: "tool.failed",
kind: "tool",
status: "failed",
sequence: 2,
taskId,
toolCallId: "tool_fixture_write",
title: "Write file failed",
payload: { failureCategory: "permission_denied" },
}),
];
const hitlActionEvents = [
event({
id: "evt_action_required",
eventClass: "action.required",
kind: "action",
status: "blocked",
sequence: 1,
taskId,
actionId: "action_fixture_approval",
title: "Approval required",
}),
event({
id: "evt_action_resolved",
eventClass: "action.resolved",
kind: "action",
status: "completed",
sequence: 2,
taskId,
actionId: "action_fixture_approval",
title: "Approval resolved",
payload: { decision: "approved" },
}),
];
const artifactEvidenceEvents = [
event({
id: "evt_artifact_changed",
eventClass: "artifact.changed",
kind: "draft",
owner: "artifact",
status: "completed",
sequence: 1,
taskId,
artifactId: "artifact_fixture_1",
artifactRefs: ["artifact_fixture_1"],
title: "Artifact changed",
}),
event({
id: "evt_evidence_changed",
eventClass: "evidence.changed",
kind: "evidence",
owner: "evidence",
status: "completed",
sequence: 2,
taskId,
evidenceId: "evidence_fixture_1",
evidenceRefs: ["evidence_fixture_1"],
title: "Evidence changed",
}),
];
const streamRepairEvents = [
event({
id: "evt_repair_delta",
eventClass: "model.delta",
kind: "model",
status: "running",
sequence: 1,
title: "Assistant text before gap",
payload: { delta: "first chunk" },
}),
event({
id: "evt_repair_snapshot",
eventClass: "snapshot.updated",
kind: "state",
status: "completed",
sequence: 3,
title: "Snapshot repaired stream",
}),
];
const subagentHandoffEvents = [
event({
id: "evt_parent_task_created",
eventClass: "task.created",
kind: "handoff",
status: "running",
sequence: 1,
taskId,
title: "Parent task created",
}),
event({
id: "evt_subagent_started",
eventClass: "subagent.started",
kind: "handoff",
status: "running",
sequence: 2,
taskId,
subagentId: "subagent_fixture_researcher",
title: "Research subagent started",
payload: { role: "researcher", parentTaskId: taskId },
}),
event({
id: "evt_subagent_tool_result",
eventClass: "tool.result",
kind: "tool",
status: "completed",
sequence: 3,
taskId,
subagentId: "subagent_fixture_researcher",
toolCallId: "tool_fixture_research",
title: "Research completed",
}),
event({
id: "evt_handoff_requested",
eventClass: "handoff.requested",
kind: "handoff",
status: "blocked",
sequence: 4,
taskId,
subagentId: "subagent_fixture_researcher",
handoffId: "handoff_fixture_1",
title: "Handoff requested",
}),
event({
id: "evt_review_verdict",
eventClass: "review.verdict",
kind: "evidence",
status: "completed",
sequence: 5,
taskId,
reviewId: "review_fixture_1",
evidenceId: "evidence_fixture_review",
evidenceRefs: ["evidence_fixture_review"],
title: "Review completed",
}),
event({
id: "evt_subagent_completed",
eventClass: "subagent.completed",
kind: "handoff",
status: "completed",
sequence: 6,
taskId,
subagentId: "subagent_fixture_researcher",
title: "Research subagent completed",
}),
];
export const agentUiConformanceFixtures = [
{
id: "text-basic",
schemaVersion: AGENT_UI_FIXTURE_SCHEMA_VERSION,
title: "Text turn with final reconciliation",
events: textBasicEvents,
finalReadModel: readModel(textBasicEvents),
expected: {
status: "completed",
messagePartCount: 1,
timelineEntryCount: 6,
},
},
{
id: "tool-success",
schemaVersion: AGENT_UI_FIXTURE_SCHEMA_VERSION,
title: "Tool call success with output reference",
events: toolSuccessEvents,
finalReadModel: readModel(toolSuccessEvents),
expected: {
status: "completed",
timelineEntryCount: 3,
},
},
{
id: "tool-failure",
schemaVersion: AGENT_UI_FIXTURE_SCHEMA_VERSION,
title: "Tool failure with recovery category",
events: toolFailureEvents,
finalReadModel: readModel(toolFailureEvents),
expected: {
status: "failed",
diagnostics: ["tool.failed"],
},
},
{
id: "hitl-action",
schemaVersion: AGENT_UI_FIXTURE_SCHEMA_VERSION,
title: "Human approval action lifecycle",
events: hitlActionEvents,
initialReadModel: readModel(hitlActionEvents.slice(0, 1), {
pendingActions: [projection(hitlActionEvents[0])],
}),
finalReadModel: readModel(hitlActionEvents),
expected: {
status: "completed",
pendingActionCount: 0,
},
},
{
id: "artifact-evidence",
schemaVersion: AGENT_UI_FIXTURE_SCHEMA_VERSION,
title: "Artifact and evidence references",
events: artifactEvidenceEvents,
finalReadModel: readModel(artifactEvidenceEvents),
expected: {
status: "completed",
artifactCount: 1,
evidenceCount: 1,
},
},
{
id: "stream-repair",
schemaVersion: AGENT_UI_FIXTURE_SCHEMA_VERSION,
title: "Interrupted stream repaired by snapshot",
events: streamRepairEvents,
finalReadModel: readModel(streamRepairEvents),
expected: {
status: "completed",
diagnostics: ["sequence_gap"],
},
},
{
id: "subagent-handoff",
schemaVersion: AGENT_UI_FIXTURE_SCHEMA_VERSION,
title: "Subagent handoff and review",
events: subagentHandoffEvents,
finalReadModel: readModel(subagentHandoffEvents, {
evidenceRefs: ["evidence_fixture_review"],
taskRefs: [taskId, "subagent_fixture_researcher"],
}),
expected: {
status: "completed",
graphNodeCount: 2,
evidenceCount: 1,
subagents: {
hasSubagents: true,
threadCount: 1,
delegationCallCount: 2,
activityCount: 4,
activeThreadCount: 0,
completedThreadCount: 1,
failedThreadCount: 0,
},
},
},
] satisfies AgentUiFixture[];
export type AgentUiConformanceFixtureId =
(typeof agentUiConformanceFixtures)[number]["id"];
export function getAgentUiFixture(
id: AgentUiConformanceFixtureId,
): AgentUiFixture {
const fixture = agentUiConformanceFixtures.find((item) => item.id === id);
if (!fixture) {
throw new Error(`Unknown Agent UI fixture: ${id}`);
}
return fixture;
}
+2
View File
@@ -1,6 +1,8 @@
export type * from "./events";
export * from "./fixtures.js";
export type * from "./graph";
export type * from "./messages";
export type * from "./projection";
export type * from "./runtime";
export type * from "./timeline";
export * from "./validation.js";
+99 -2
View File
@@ -36,8 +36,20 @@ export interface AgentUiRuntimeStatusView {
export interface AgentUiRefView {
id: string;
sourceEventId: string;
title?: string;
status?: AgentRuntimeExecutionEventStatus;
owner?: "artifact" | "evidence" | "runtime" | "ui" | string;
path?: string;
contentRef?: string;
mimeType?: string;
preview?: string;
metadata?: Record<string, unknown>;
}
export type AgentUiArtifactRefView = AgentUiRefView;
export type AgentUiEvidenceRefView = AgentUiRefView;
export interface AgentUiDiagnosticView {
id: string;
sourceEventId: string;
@@ -46,6 +58,90 @@ export interface AgentUiDiagnosticView {
status: AgentRuntimeExecutionEventStatus;
}
export type AgentUiSubagentCommand =
| "open_detail"
| "send_input"
| "interrupt"
| "close"
| "wait"
| string;
export interface AgentUiSubagentIsolationView {
runtimeProfileId?: string;
modelProfileId?: string;
isolationProfileId?: string;
workspaceRef?: string;
permissionProfile?: string;
sandboxProfile?: string;
forkPolicy?: string;
depth?: number;
canDelegate?: boolean;
}
export interface AgentUiSubagentThreadView {
threadId: string;
subagentId: string;
parentThreadId?: string;
parentTaskId?: string;
taskId?: string;
taskPath?: string;
role?: string;
nickname?: string;
status: AgentRuntimeExecutionEventStatus;
title: string;
summary?: string;
promptPreview?: string;
lastActivityAt?: string;
createdAt?: string;
completedAt?: string;
artifactRefs: string[];
evidenceRefs: string[];
sourceEventIds: string[];
isolation?: AgentUiSubagentIsolationView;
}
export interface AgentUiSubagentDelegationView {
callId: string;
sourceEventId: string;
action: "spawn" | "handoff" | "send_input" | "wait" | "interrupt" | "close" | string;
parentThreadId?: string;
targetThreadIds: string[];
status: AgentRuntimeExecutionEventStatus;
title: string;
promptPreview?: string;
createdAt?: string;
completedAt?: string;
}
export interface AgentUiSubagentActivityView {
activityId: string;
threadId: string;
sourceEventId: string;
kind:
| "started"
| "updated"
| "interacted"
| "handoff"
| "review"
| "completed"
| "interrupted"
| "failed"
| string;
status: AgentRuntimeExecutionEventStatus;
title: string;
createdAt?: string;
}
export interface AgentUiSubagentsModel {
hasSubagents: boolean;
threads: AgentUiSubagentThreadView[];
delegationCalls: AgentUiSubagentDelegationView[];
activities: AgentUiSubagentActivityView[];
activeThreadIds: string[];
completedThreadIds: string[];
failedThreadIds: string[];
}
export interface AgentUiProjectionState<
TEvent extends AgentRuntimeExecutionEvent = AgentRuntimeExecutionEvent,
> {
@@ -55,9 +151,10 @@ export interface AgentUiProjectionState<
graph: ExecutionGraph;
tools: AgentRuntimeEventProjection<TEvent>[];
actions: AgentRuntimeEventProjection<TEvent>[];
artifacts: AgentUiRefView[];
evidence: AgentUiRefView[];
artifacts: AgentUiArtifactRefView[];
evidence: AgentUiEvidenceRefView[];
diagnostics: AgentUiDiagnosticView[];
subagents: AgentUiSubagentsModel;
readModel: AgentRuntimeReadModel<TEvent>;
hydration: {
status: AgentUiHydrationStatus;
@@ -82,10 +82,15 @@ export interface AgentRuntimeExecutionEvent {
threadId?: string;
turnId?: string;
taskId?: string;
subagentId?: string;
runId?: string;
stepId?: string;
toolCallId?: string;
actionId?: string;
workerId?: string;
channelId?: string;
handoffId?: string;
reviewId?: string;
traceId?: string;
spanId?: string;
attemptId?: string;
@@ -0,0 +1,935 @@
import type { AgentUiFixture } from "./fixtures";
import type { AgentUiProjectionState } from "./projection";
import type {
AgentRuntimeExecutionEvent,
AgentRuntimeReadModel,
} from "./runtime";
export type AgentUiContractValidationCode =
| "schema_mismatch"
| "missing_scope_id"
| "sequence_gap"
| "secret_leak_risk"
| "large_payload_inline"
| "unknown_event_type";
export interface AgentUiContractValidationIssue {
code: AgentUiContractValidationCode;
path: string;
message: string;
}
export class AgentUiContractValidationError extends Error {
readonly issues: AgentUiContractValidationIssue[];
constructor(issues: AgentUiContractValidationIssue[]) {
super(issues.map((issue) => `${issue.path}: ${issue.message}`).join("; "));
this.name = "AgentUiContractValidationError";
this.issues = issues;
}
}
export function validateRuntimeEvent(
input: unknown,
): AgentRuntimeExecutionEvent {
const issues = collectRuntimeEventValidationIssues(input);
throwIfIssues(issues);
return input as AgentRuntimeExecutionEvent;
}
export function validateThreadReadModel(
input: unknown,
): AgentRuntimeReadModel {
const issues = collectThreadReadModelValidationIssues(input);
throwIfIssues(issues);
return input as AgentRuntimeReadModel;
}
export function validateProjectionState(
input: unknown,
): AgentUiProjectionState {
const issues = collectProjectionStateValidationIssues(input);
throwIfIssues(issues);
return input as AgentUiProjectionState;
}
export function validateAgentUiFixture(input: unknown): AgentUiFixture {
const issues = collectAgentUiFixtureValidationIssues(input);
throwIfIssues(issues);
return input as AgentUiFixture;
}
export function collectRuntimeEventValidationIssues(
input: unknown,
path = "$",
): AgentUiContractValidationIssue[] {
const issues: AgentUiContractValidationIssue[] = [];
if (!isRecord(input)) {
return [
issue("schema_mismatch", path, "Runtime event must be an object."),
];
}
requireString(input, "id", path, issues);
requireString(input, "schemaVersion", path, issues);
requireString(input, "runtimeId", path, issues);
requireString(input, "kind", path, issues);
requireString(input, "status", path, issues);
requireString(input, "title", path, issues);
requireString(input, "createdAt", path, issues);
if (!Number.isInteger(input.sequence)) {
issues.push(
issue(
"schema_mismatch",
`${path}.sequence`,
"Runtime event sequence must be an integer.",
),
);
}
if (typeof input.eventClass === "string") {
collectScopeIssues(input, path, issues);
}
collectPayloadIssues(input.payload, `${path}.payload`, issues);
return issues;
}
export function collectThreadReadModelValidationIssues(
input: unknown,
path = "$",
): AgentUiContractValidationIssue[] {
const issues: AgentUiContractValidationIssue[] = [];
if (!isRecord(input)) {
return [
issue("schema_mismatch", path, "Thread read model must be an object."),
];
}
requireArray(input, "events", path, issues);
if (Array.isArray(input.events)) {
input.events.forEach((item, index) => {
collectRuntimeEventProjectionValidationIssues(
item,
`${path}.events[${index}]`,
issues,
);
});
}
requireArray(input, "visibleEvents", path, issues);
if (Array.isArray(input.visibleEvents)) {
input.visibleEvents.forEach((item, index) => {
collectRuntimeEventProjectionValidationIssues(
item,
`${path}.visibleEvents[${index}]`,
issues,
);
});
}
requireArray(input, "pendingActions", path, issues);
if (Array.isArray(input.pendingActions)) {
input.pendingActions.forEach((item, index) => {
collectRuntimeEventProjectionValidationIssues(
item,
`${path}.pendingActions[${index}]`,
issues,
);
});
}
requireBoolean(input, "inputSourceRecovery", path, issues);
requireNumber(input, "sourceCount", path, issues);
requireStringArray(input, "artifactRefs", path, issues);
requireStringArray(input, "evidenceRefs", path, issues);
requireStringArray(input, "taskRefs", path, issues);
return issues;
}
export function collectProjectionStateValidationIssues(
input: unknown,
path = "$",
): AgentUiContractValidationIssue[] {
const issues: AgentUiContractValidationIssue[] = [];
if (!isRecord(input)) {
return [
issue("schema_mismatch", path, "Projection state must be an object."),
];
}
requireRecord(input, "runtime", path, issues);
requireArray(input, "messages", path, issues);
if (Array.isArray(input.messages)) {
input.messages.forEach((item, index) => {
collectMessagePartValidationIssues(
item,
`${path}.messages[${index}]`,
issues,
);
});
}
requireArray(input, "timeline", path, issues);
if (Array.isArray(input.timeline)) {
input.timeline.forEach((item, index) => {
collectTimelineEntryValidationIssues(
item,
`${path}.timeline[${index}]`,
issues,
);
});
}
requireArray(input, "graph", path, issues);
if (Array.isArray(input.graph)) {
input.graph.forEach((item, index) => {
collectGraphNodeValidationIssues(item, `${path}.graph[${index}]`, issues);
});
}
requireArray(input, "tools", path, issues);
if (Array.isArray(input.tools)) {
input.tools.forEach((item, index) => {
collectRuntimeEventProjectionValidationIssues(
item,
`${path}.tools[${index}]`,
issues,
);
});
}
requireArray(input, "actions", path, issues);
if (Array.isArray(input.actions)) {
input.actions.forEach((item, index) => {
collectRuntimeEventProjectionValidationIssues(
item,
`${path}.actions[${index}]`,
issues,
);
});
}
requireArray(input, "artifacts", path, issues);
if (Array.isArray(input.artifacts)) {
input.artifacts.forEach((item, index) => {
collectRefValidationIssues(item, `${path}.artifacts[${index}]`, issues);
});
}
requireArray(input, "evidence", path, issues);
if (Array.isArray(input.evidence)) {
input.evidence.forEach((item, index) => {
collectRefValidationIssues(item, `${path}.evidence[${index}]`, issues);
});
}
requireArray(input, "diagnostics", path, issues);
if (Array.isArray(input.diagnostics)) {
input.diagnostics.forEach((item, index) => {
collectDiagnosticValidationIssues(
item,
`${path}.diagnostics[${index}]`,
issues,
);
});
}
requireRecord(input, "subagents", path, issues);
if (isRecord(input.subagents)) {
collectSubagentsValidationIssues(
input.subagents,
`${path}.subagents`,
issues,
);
}
requireRecord(input, "readModel", path, issues);
if (isRecord(input.readModel)) {
issues.push(
...collectThreadReadModelValidationIssues(
input.readModel,
`${path}.readModel`,
),
);
}
requireRecord(input, "hydration", path, issues);
if (isRecord(input.hydration)) {
requireString(input.hydration, "status", `${path}.hydration`, issues);
requireNumber(input.hydration, "eventCount", `${path}.hydration`, issues);
}
requireRecord(input, "ephemeralUi", path, issues);
return issues;
}
function collectMessagePartValidationIssues(
input: unknown,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (!isRecord(input)) {
issues.push(issue("schema_mismatch", path, "Message part must be an object."));
return;
}
requireString(input, "type", path, issues);
requireString(input, "partId", path, issues);
requireString(input, "sourceEventId", path, issues);
optionalString(input, "messageId", path, issues);
optionalString(input, "role", path, issues);
optionalString(input, "text", path, issues);
optionalString(input, "state", path, issues);
optionalString(input, "toolCallId", path, issues);
optionalString(input, "artifactId", path, issues);
optionalString(input, "evidenceId", path, issues);
optionalString(input, "diagnosticId", path, issues);
optionalString(input, "createdAt", path, issues);
requireStringArray(input, "refs", path, issues);
}
function collectTimelineEntryValidationIssues(
input: unknown,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (!isRecord(input)) {
issues.push(issue("schema_mismatch", path, "Timeline entry must be an object."));
return;
}
requireString(input, "entryId", path, issues);
requireString(input, "kind", path, issues);
requireString(input, "status", path, issues);
requireString(input, "title", path, issues);
requireString(input, "sourceEventId", path, issues);
requireString(input, "createdAt", path, issues);
optionalString(input, "phase", path, issues);
optionalString(input, "owner", path, issues);
optionalString(input, "detail", path, issues);
optionalString(input, "completedAt", path, issues);
requireStringArray(input, "refs", path, issues);
if ("sequence" in input && input.sequence !== undefined) {
requireNumber(input, "sequence", path, issues);
}
}
function collectGraphNodeValidationIssues(
input: unknown,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (!isRecord(input)) {
issues.push(issue("schema_mismatch", path, "Graph node must be an object."));
return;
}
requireString(input, "nodeId", path, issues);
requireString(input, "nodeType", path, issues);
requireString(input, "status", path, issues);
requireString(input, "title", path, issues);
optionalString(input, "parentId", path, issues);
optionalString(input, "createdAt", path, issues);
optionalString(input, "completedAt", path, issues);
requireStringArray(input, "refs", path, issues);
requireStringArray(input, "sourceEventIds", path, issues);
}
function collectRuntimeEventProjectionValidationIssues(
input: unknown,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (!isRecord(input)) {
issues.push(issue("schema_mismatch", path, "Runtime projection event must be an object."));
return;
}
requireString(input, "id", path, issues);
requireRecord(input, "source", path, issues);
requireString(input, "surface", path, issues);
requireString(input, "title", path, issues);
requireString(input, "status", path, issues);
requireString(input, "displayStatusKey", path, issues);
requireBoolean(input, "resolved", path, issues);
requireString(input, "actionKind", path, issues);
requireString(input, "targetModule", path, issues);
optionalString(input, "detail", path, issues);
optionalString(input, "actionId", path, issues);
if (isRecord(input.source)) {
issues.push(...collectRuntimeEventValidationIssues(input.source, `${path}.source`));
}
}
function collectRefValidationIssues(
input: unknown,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (!isRecord(input)) {
issues.push(issue("schema_mismatch", path, "Runtime ref must be an object."));
return;
}
requireString(input, "id", path, issues);
requireString(input, "sourceEventId", path, issues);
optionalString(input, "title", path, issues);
optionalString(input, "status", path, issues);
optionalString(input, "owner", path, issues);
optionalString(input, "path", path, issues);
optionalString(input, "contentRef", path, issues);
optionalString(input, "mimeType", path, issues);
optionalString(input, "preview", path, issues);
if (typeof input.preview === "string" && input.preview.length > 512) {
issues.push(
issue(
"large_payload_inline",
`${path}.preview`,
"Runtime ref preview must stay small; store large content behind contentRef.",
),
);
}
if ("metadata" in input && input.metadata !== undefined) {
requireRecord(input, "metadata", path, issues);
collectPayloadIssues(input.metadata, `${path}.metadata`, issues);
}
}
function collectDiagnosticValidationIssues(
input: unknown,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (!isRecord(input)) {
issues.push(issue("schema_mismatch", path, "Diagnostic must be an object."));
return;
}
requireString(input, "id", path, issues);
requireString(input, "sourceEventId", path, issues);
requireString(input, "title", path, issues);
requireString(input, "status", path, issues);
optionalString(input, "detail", path, issues);
}
function collectSubagentsValidationIssues(
input: Record<string, unknown>,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
requireBoolean(input, "hasSubagents", path, issues);
requireArray(input, "threads", path, issues);
requireArray(input, "delegationCalls", path, issues);
requireArray(input, "activities", path, issues);
requireStringArray(input, "activeThreadIds", path, issues);
requireStringArray(input, "completedThreadIds", path, issues);
requireStringArray(input, "failedThreadIds", path, issues);
if (Array.isArray(input.threads)) {
input.threads.forEach((item, index) => {
collectSubagentThreadValidationIssues(
item,
`${path}.threads[${index}]`,
issues,
);
});
}
if (Array.isArray(input.delegationCalls)) {
input.delegationCalls.forEach((item, index) => {
collectSubagentDelegationValidationIssues(
item,
`${path}.delegationCalls[${index}]`,
issues,
);
});
}
if (Array.isArray(input.activities)) {
input.activities.forEach((item, index) => {
collectSubagentActivityValidationIssues(
item,
`${path}.activities[${index}]`,
issues,
);
});
}
}
function collectSubagentThreadValidationIssues(
input: unknown,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (!isRecord(input)) {
issues.push(issue("schema_mismatch", path, "Subagent thread must be an object."));
return;
}
requireString(input, "threadId", path, issues);
requireString(input, "subagentId", path, issues);
requireString(input, "status", path, issues);
requireString(input, "title", path, issues);
optionalString(input, "parentThreadId", path, issues);
optionalString(input, "parentTaskId", path, issues);
optionalString(input, "taskId", path, issues);
optionalString(input, "taskPath", path, issues);
optionalString(input, "role", path, issues);
optionalString(input, "nickname", path, issues);
optionalString(input, "summary", path, issues);
optionalString(input, "promptPreview", path, issues);
optionalString(input, "lastActivityAt", path, issues);
optionalString(input, "createdAt", path, issues);
optionalString(input, "completedAt", path, issues);
requireStringArray(input, "artifactRefs", path, issues);
requireStringArray(input, "evidenceRefs", path, issues);
requireStringArray(input, "sourceEventIds", path, issues);
if ("isolation" in input && input.isolation !== undefined) {
requireRecord(input, "isolation", path, issues);
if (isRecord(input.isolation)) {
collectSubagentIsolationValidationIssues(
input.isolation,
`${path}.isolation`,
issues,
);
}
}
}
function collectSubagentIsolationValidationIssues(
input: Record<string, unknown>,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
optionalString(input, "runtimeProfileId", path, issues);
optionalString(input, "modelProfileId", path, issues);
optionalString(input, "isolationProfileId", path, issues);
optionalString(input, "workspaceRef", path, issues);
optionalString(input, "permissionProfile", path, issues);
optionalString(input, "sandboxProfile", path, issues);
optionalString(input, "forkPolicy", path, issues);
if ("depth" in input && input.depth !== undefined) {
requireNumber(input, "depth", path, issues);
}
if ("canDelegate" in input && input.canDelegate !== undefined) {
requireBoolean(input, "canDelegate", path, issues);
}
}
function collectSubagentDelegationValidationIssues(
input: unknown,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (!isRecord(input)) {
issues.push(issue("schema_mismatch", path, "Subagent delegation must be an object."));
return;
}
requireString(input, "callId", path, issues);
requireString(input, "sourceEventId", path, issues);
requireString(input, "action", path, issues);
requireString(input, "status", path, issues);
requireString(input, "title", path, issues);
optionalString(input, "parentThreadId", path, issues);
optionalString(input, "promptPreview", path, issues);
optionalString(input, "createdAt", path, issues);
optionalString(input, "completedAt", path, issues);
requireStringArray(input, "targetThreadIds", path, issues);
}
function collectSubagentActivityValidationIssues(
input: unknown,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (!isRecord(input)) {
issues.push(issue("schema_mismatch", path, "Subagent activity must be an object."));
return;
}
requireString(input, "activityId", path, issues);
requireString(input, "threadId", path, issues);
requireString(input, "sourceEventId", path, issues);
requireString(input, "kind", path, issues);
requireString(input, "status", path, issues);
requireString(input, "title", path, issues);
optionalString(input, "createdAt", path, issues);
}
export function collectAgentUiFixtureValidationIssues(
input: unknown,
path = "$",
): AgentUiContractValidationIssue[] {
const issues: AgentUiContractValidationIssue[] = [];
if (!isRecord(input)) {
return [issue("schema_mismatch", path, "Fixture must be an object.")];
}
requireString(input, "id", path, issues);
requireString(input, "schemaVersion", path, issues);
requireString(input, "title", path, issues);
requireArray(input, "events", path, issues);
requireRecord(input, "expected", path, issues);
if (isRecord(input.expected)) {
collectFixtureExpectationValidationIssues(
input.expected,
`${path}.expected`,
issues,
);
}
if (Array.isArray(input.events)) {
input.events.forEach((event, index) => {
issues.push(
...collectRuntimeEventValidationIssues(event, `${path}.events[${index}]`),
);
});
issues.push(...collectSequenceIssues(input, path));
}
if ("initialReadModel" in input && input.initialReadModel !== undefined) {
issues.push(
...collectThreadReadModelValidationIssues(
input.initialReadModel,
`${path}.initialReadModel`,
),
);
}
if ("finalReadModel" in input && input.finalReadModel !== undefined) {
issues.push(
...collectThreadReadModelValidationIssues(
input.finalReadModel,
`${path}.finalReadModel`,
),
);
}
return issues;
}
function collectFixtureExpectationValidationIssues(
input: Record<string, unknown>,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
requireString(input, "status", path, issues);
if ("subagents" in input && input.subagents !== undefined) {
requireRecord(input, "subagents", path, issues);
if (isRecord(input.subagents)) {
collectFixtureSubagentsExpectationValidationIssues(
input.subagents,
`${path}.subagents`,
issues,
);
}
}
}
function collectFixtureSubagentsExpectationValidationIssues(
input: Record<string, unknown>,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
const booleanFields = ["hasSubagents"];
const numberFields = [
"threadCount",
"delegationCallCount",
"activityCount",
"activeThreadCount",
"completedThreadCount",
"failedThreadCount",
];
booleanFields.forEach((field) => {
if (field in input && input[field] !== undefined) {
requireBoolean(input, field, path, issues);
}
});
numberFields.forEach((field) => {
if (field in input && input[field] !== undefined) {
requireNumber(input, field, path, issues);
}
});
}
function collectSequenceIssues(
fixture: Record<string, unknown>,
path: string,
): AgentUiContractValidationIssue[] {
if (!Array.isArray(fixture.events)) {
return [];
}
const expectedDiagnostics = isRecord(fixture.expected)
&& Array.isArray(fixture.expected.diagnostics)
? fixture.expected.diagnostics
: [];
const allowsSequenceGap = expectedDiagnostics.includes("sequence_gap");
if (allowsSequenceGap) {
return [];
}
const issues: AgentUiContractValidationIssue[] = [];
let previousSequence: number | undefined;
fixture.events.forEach((event, index) => {
if (!isRecord(event) || !Number.isInteger(event.sequence)) {
return;
}
const sequence = event.sequence as number;
if (
previousSequence !== undefined
&& sequence !== previousSequence + 1
) {
issues.push(
issue(
"sequence_gap",
`${path}.events[${index}].sequence`,
"Runtime event sequence must be contiguous unless the fixture declares sequence_gap diagnostics.",
),
);
}
previousSequence = sequence;
});
return issues;
}
function collectScopeIssues(
event: Record<string, unknown>,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
const eventClass = String(event.eventClass);
if (eventClass.startsWith("tool.") && typeof event.toolCallId !== "string") {
issues.push(
issue(
"missing_scope_id",
`${path}.toolCallId`,
"Tool events must include toolCallId.",
),
);
}
if (
eventClass.startsWith("action.")
&& typeof event.actionId !== "string"
) {
issues.push(
issue(
"missing_scope_id",
`${path}.actionId`,
"Action events must include actionId.",
),
);
}
if (
eventClass.startsWith("artifact.")
&& typeof event.artifactId !== "string"
&& !hasNonEmptyStringArray(event.artifactRefs)
) {
issues.push(
issue(
"missing_scope_id",
`${path}.artifactId`,
"Artifact events must include artifactId or artifactRefs.",
),
);
}
if (
(eventClass.startsWith("evidence.") || eventClass.startsWith("review."))
&& typeof event.evidenceId !== "string"
&& !hasNonEmptyStringArray(event.evidenceRefs)
) {
issues.push(
issue(
"missing_scope_id",
`${path}.evidenceId`,
"Evidence and review events must include evidenceId or evidenceRefs.",
),
);
}
}
function collectPayloadIssues(
input: unknown,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (input === undefined) {
return;
}
const serialized = safeStringify(input);
if (serialized.length > 32_000) {
issues.push(
issue(
"large_payload_inline",
path,
"Large runtime payloads must be stored as refs instead of inline event payloads.",
),
);
}
scanSecretKeys(input, path, issues);
}
function scanSecretKeys(
input: unknown,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (!isRecord(input) && !Array.isArray(input)) {
return;
}
Object.entries(input).forEach(([key, value]) => {
const childPath = Array.isArray(input) ? `${path}[${key}]` : `${path}.${key}`;
if (/(api[-_]?key|authorization|password|secret|token)/i.test(key)) {
issues.push(
issue(
"secret_leak_risk",
childPath,
"Runtime payload appears to contain a secret-bearing field.",
),
);
return;
}
scanSecretKeys(value, childPath, issues);
});
}
function requireString(
input: Record<string, unknown>,
field: string,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (typeof input[field] !== "string" || input[field] === "") {
issues.push(
issue(
"schema_mismatch",
`${path}.${field}`,
`${field} must be a non-empty string.`,
),
);
}
}
function requireNumber(
input: Record<string, unknown>,
field: string,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (typeof input[field] !== "number" || !Number.isFinite(input[field])) {
issues.push(
issue("schema_mismatch", `${path}.${field}`, `${field} must be a number.`),
);
}
}
function requireBoolean(
input: Record<string, unknown>,
field: string,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (typeof input[field] !== "boolean") {
issues.push(
issue(
"schema_mismatch",
`${path}.${field}`,
`${field} must be a boolean.`,
),
);
}
}
function requireArray(
input: Record<string, unknown>,
field: string,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (!Array.isArray(input[field])) {
issues.push(
issue("schema_mismatch", `${path}.${field}`, `${field} must be an array.`),
);
}
}
function requireRecord(
input: Record<string, unknown>,
field: string,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (!isRecord(input[field])) {
issues.push(
issue("schema_mismatch", `${path}.${field}`, `${field} must be an object.`),
);
}
}
function optionalString(
input: Record<string, unknown>,
field: string,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (field in input && input[field] !== undefined && typeof input[field] !== "string") {
issues.push(
issue("schema_mismatch", `${path}.${field}`, `${field} must be a string.`),
);
}
}
function requireStringArray(
input: Record<string, unknown>,
field: string,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
requireArray(input, field, path, issues);
if (!Array.isArray(input[field])) return;
input[field].forEach((item, index) => {
if (typeof item !== "string") {
issues.push(
issue(
"schema_mismatch",
`${path}.${field}[${index}]`,
`${field} entries must be strings.`,
),
);
}
});
}
function optionalStringArray(
input: Record<string, unknown>,
field: string,
path: string,
issues: AgentUiContractValidationIssue[],
): void {
if (!(field in input) || input[field] === undefined) return;
requireStringArray(input, field, path, issues);
}
function throwIfIssues(issues: AgentUiContractValidationIssue[]): void {
if (issues.length > 0) {
throw new AgentUiContractValidationError(issues);
}
}
function issue(
code: AgentUiContractValidationCode,
path: string,
message: string,
): AgentUiContractValidationIssue {
return { code, path, message };
}
function isRecord(input: unknown): input is Record<string, unknown> {
return typeof input === "object" && input !== null && !Array.isArray(input);
}
function hasNonEmptyStringArray(input: unknown): boolean {
return (
Array.isArray(input)
&& input.some((item) => typeof item === "string" && item.length > 0)
);
}
function safeStringify(input: unknown): string {
try {
return JSON.stringify(input) ?? "";
} catch {
return "";
}
}
@@ -2,45 +2,375 @@ import assert from "node:assert/strict";
import fs from "node:fs/promises";
import test from "node:test";
import {} from "../dist/index.js";
import {
AgentUiContractValidationError,
agentUiConformanceFixtures,
collectAgentUiFixtureValidationIssues,
collectProjectionStateValidationIssues,
collectRuntimeEventValidationIssues,
getAgentUiFixture,
validateAgentUiFixture,
validateProjectionState,
validateRuntimeEvent,
} from "../dist/index.js";
test("agent ui contracts package builds a runtime-free module", () => {
assert.equal(true, true);
test("agent ui contracts package exports runtime-free validation and fixtures", () => {
assert.equal(typeof validateRuntimeEvent, "function");
assert.equal(typeof validateAgentUiFixture, "function");
assert.equal(typeof getAgentUiFixture, "function");
assert.equal(Array.isArray(agentUiConformanceFixtures), true);
});
test("agent ui contracts publish adapter and runtime type declarations", async () => {
const declarations = await import("../dist/index.js");
assert.deepEqual(Object.keys(declarations), []);
assert.deepEqual(Object.keys(declarations).sort(), [
"AGENT_UI_FIXTURE_SCHEMA_VERSION",
"AgentUiContractValidationError",
"agentUiConformanceFixtures",
"collectAgentUiFixtureValidationIssues",
"collectProjectionStateValidationIssues",
"collectRuntimeEventValidationIssues",
"collectThreadReadModelValidationIssues",
"getAgentUiFixture",
"validateAgentUiFixture",
"validateProjectionState",
"validateRuntimeEvent",
"validateThreadReadModel",
]);
const indexDeclarations = await readDeclaration("index");
assert.equal(
indexDeclarations,
[
'export type * from "./events";',
'export * from "./fixtures.js";',
'export type * from "./graph";',
'export type * from "./messages";',
'export type * from "./projection";',
'export type * from "./runtime";',
'export type * from "./timeline";',
'export * from "./validation.js";',
"",
].join("\n"),
);
const typeDeclarations = [
await readDeclaration("events"),
await readDeclaration("fixtures"),
await readDeclaration("runtime"),
await readDeclaration("projection"),
await readDeclaration("messages"),
await readDeclaration("timeline"),
await readDeclaration("graph"),
await readDeclaration("validation"),
].join("\n");
assert.match(typeDeclarations, /export type AgentUiEventClass/);
assert.match(typeDeclarations, /export interface AgentUiProjectionEvent/);
assert.match(typeDeclarations, /export interface AgentRuntimeExecutionEvent/);
assert.match(typeDeclarations, /subagentId\?: string/);
assert.match(typeDeclarations, /handoffId\?: string/);
assert.match(typeDeclarations, /reviewId\?: string/);
assert.match(typeDeclarations, /export type AgentUiArtifactRefView/);
assert.match(typeDeclarations, /export type AgentUiEvidenceRefView/);
assert.match(typeDeclarations, /export interface AgentUiSubagentsModel/);
assert.match(typeDeclarations, /export interface AgentUiProjectionState/);
assert.match(typeDeclarations, /artifacts: AgentUiArtifactRefView\[\]/);
assert.match(typeDeclarations, /evidence: AgentUiEvidenceRefView\[\]/);
assert.match(typeDeclarations, /subagents: AgentUiSubagentsModel/);
assert.match(typeDeclarations, /actions\?: AgentRuntimeActionProjection\[\]/);
assert.match(typeDeclarations, /export interface AgentUiFixture/);
assert.match(typeDeclarations, /export declare function validateRuntimeEvent/);
});
test("projection state validation requires Subagents model", () => {
const projectionState = {
runtime: { status: "idle" },
messages: [],
timeline: [],
graph: [],
tools: [],
actions: [],
artifacts: [],
evidence: [],
diagnostics: [],
subagents: {
hasSubagents: false,
threads: [],
delegationCalls: [],
activities: [],
activeThreadIds: [],
completedThreadIds: [],
failedThreadIds: [],
},
readModel: {
events: [],
visibleEvents: [],
pendingActions: [],
inputSourceRecovery: false,
sourceCount: 0,
artifactRefs: [],
evidenceRefs: [],
taskRefs: [],
},
hydration: { status: "idle", eventCount: 0 },
ephemeralUi: {},
};
assert.equal(validateProjectionState(projectionState), projectionState);
const withoutSubagents = { ...projectionState };
delete withoutSubagents.subagents;
assert.deepEqual(
collectProjectionStateValidationIssues(withoutSubagents).map(
(issue) => issue.path,
),
["$.subagents"],
);
const withIncompleteSubagents = {
...projectionState,
subagents: {
hasSubagents: false,
threads: [],
},
};
assert.deepEqual(
collectProjectionStateValidationIssues(withIncompleteSubagents).map(
(issue) => issue.path,
),
[
"$.subagents.delegationCalls",
"$.subagents.activities",
"$.subagents.activeThreadIds",
"$.subagents.completedThreadIds",
"$.subagents.failedThreadIds",
],
);
});
test("projection state validation checks structured artifact and evidence refs", () => {
const projectionState = {
runtime: { status: "completed" },
messages: [],
timeline: [],
graph: [],
tools: [],
actions: [],
artifacts: [
{
id: "prompt-draft:1",
sourceEventId: "evt-artifact",
title: "Prompt draft",
status: "completed",
owner: "artifact",
path: "drafts/prompt.md",
contentRef: "content://prompt-draft/1",
mimeType: "text/markdown",
preview: "可编辑草稿",
metadata: { draftId: "1" },
},
],
evidence: [
{
id: "input-source:1",
sourceEventId: "evt-evidence",
title: "Input source",
status: "completed",
owner: "evidence",
path: "evidence/input-source-1",
mimeType: "application/json",
preview: "证据摘要",
metadata: { sourceId: "1" },
},
],
diagnostics: [],
subagents: {
hasSubagents: false,
threads: [],
delegationCalls: [],
activities: [],
activeThreadIds: [],
completedThreadIds: [],
failedThreadIds: [],
},
readModel: {
events: [],
visibleEvents: [],
pendingActions: [],
inputSourceRecovery: false,
sourceCount: 0,
artifactRefs: ["prompt-draft:1"],
evidenceRefs: ["input-source:1"],
taskRefs: [],
},
hydration: { status: "live", eventCount: 2 },
ephemeralUi: {},
};
assert.equal(validateProjectionState(projectionState), projectionState);
const invalid = {
...projectionState,
artifacts: [
{ id: "prompt-draft:1" },
{
id: "prompt-draft:2",
sourceEventId: "evt-artifact-2",
preview: "x".repeat(513),
},
],
evidence: [
{
id: "input-source:1",
sourceEventId: "evt-evidence",
metadata: { token: "secret" },
},
],
};
assert.deepEqual(
collectProjectionStateValidationIssues(invalid).map((issue) => [
issue.code,
issue.path,
]),
[
["schema_mismatch", "$.artifacts[0].sourceEventId"],
["large_payload_inline", "$.artifacts[1].preview"],
["secret_leak_risk", "$.evidence[0].metadata.token"],
],
);
});
test("agent ui conformance fixtures cover the standard runtime slices", () => {
assert.deepEqual(
agentUiConformanceFixtures.map((fixture) => fixture.id),
[
"text-basic",
"tool-success",
"tool-failure",
"hitl-action",
"artifact-evidence",
"stream-repair",
"subagent-handoff",
],
);
for (const fixture of agentUiConformanceFixtures) {
assert.equal(collectAgentUiFixtureValidationIssues(fixture).length, 0);
assert.equal(validateAgentUiFixture(fixture), fixture);
}
const handoff = getAgentUiFixture("subagent-handoff");
assert.deepEqual(handoff.expected.subagents, {
hasSubagents: true,
threadCount: 1,
delegationCallCount: 2,
activityCount: 4,
activeThreadCount: 0,
completedThreadCount: 1,
failedThreadCount: 0,
});
assert.ok(
handoff.events.some((event) => event.eventClass === "subagent.started"),
);
assert.ok(
handoff.events.some((event) => event.eventClass === "handoff.requested"),
);
assert.ok(handoff.events.some((event) => event.eventClass === "review.verdict"));
});
test("fixture validation checks Subagents expectation shape", () => {
const fixture = {
...getAgentUiFixture("subagent-handoff"),
expected: {
status: "completed",
subagents: {
hasSubagents: "yes",
threadCount: "1",
},
},
};
assert.deepEqual(
collectAgentUiFixtureValidationIssues(fixture).map((issue) => issue.path),
[
"$.expected.subagents.hasSubagents",
"$.expected.subagents.threadCount",
],
);
});
test("runtime event validation enforces scope ids and payload safety", () => {
const validToolEvent = {
id: "evt_tool",
schemaVersion: "lime-runtime-event/v0.1",
runtimeId: "runtime",
threadId: "thread",
turnId: "turn",
sequence: 1,
kind: "tool",
status: "running",
eventClass: "tool.started",
toolCallId: "tool_1",
title: "Tool started",
createdAt: "2026-06-10T00:00:00.000Z",
};
assert.equal(validateRuntimeEvent(validToolEvent), validToolEvent);
assert.deepEqual(
collectRuntimeEventValidationIssues({
...validToolEvent,
toolCallId: undefined,
}).map((issue) => issue.code),
["missing_scope_id"],
);
assert.deepEqual(
collectRuntimeEventValidationIssues({
...validToolEvent,
payload: { apiKey: "secret" },
}).map((issue) => issue.code),
["secret_leak_risk"],
);
assert.deepEqual(
collectRuntimeEventValidationIssues({
...validToolEvent,
payload: { text: "x".repeat(33_000) },
}).map((issue) => issue.code),
["large_payload_inline"],
);
});
test("fixture validation reports sequence gaps unless the fixture declares repair", () => {
const base = getAgentUiFixture("text-basic");
const broken = {
...base,
events: base.events.map((event, index) =>
index === 2 ? { ...event, sequence: 8 } : event,
),
};
assert.deepEqual(
collectAgentUiFixtureValidationIssues(broken)
.map((issue) => issue.code)
.filter((code) => code === "sequence_gap"),
["sequence_gap", "sequence_gap"],
);
assert.throws(
() => validateAgentUiFixture(broken),
AgentUiContractValidationError,
);
assert.equal(
collectAgentUiFixtureValidationIssues(getAgentUiFixture("stream-repair"))
.length,
0,
);
});
async function readDeclaration(name) {
+4 -4
View File
@@ -1,6 +1,6 @@
# app-server-client
# @limecloud/app-server-client
`app-server-client` is the TypeScript client surface for independent apps
`@limecloud/app-server-client` is the TypeScript client surface for independent apps
that talk to `app-server` over JSON-RPC.
Current scope:
@@ -34,7 +34,7 @@ Electron main integration shape:
import {
AppServerAgentEventRouter,
startPackagedAppServerSidecar,
} from "app-server-client";
} from "@limecloud/app-server-client";
const { connected, lifecycle } = await startPackagedAppServerSidecar(
{
@@ -87,7 +87,7 @@ void (async () => {
Agent runtime SDK facade:
```ts
import { createAgentRuntimeClient } from "app-server-client";
import { createAgentRuntimeClient } from "@limecloud/app-server-client";
const runtime = createAgentRuntimeClient(connection, {
request: { timeoutMs: 120_000 },
+14 -2
View File
@@ -1,9 +1,21 @@
{
"name": "app-server-client",
"version": "1.65.0",
"name": "@limecloud/app-server-client",
"version": "1.66.0",
"type": "module",
"description": "App Server JSON-RPC client and sidecar launch helpers for independent apps.",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/limecloud/lime.git",
"directory": "packages/app-server-client"
},
"homepage": "https://github.com/limecloud/lime/tree/main/packages/app-server-client#readme",
"bugs": {
"url": "https://github.com/limecloud/lime/issues"
},
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=20.0.0"
},
+18
View File
@@ -243,6 +243,7 @@ import {
METHOD_WORKSPACE_BY_PATH_READ,
METHOD_WORKSPACE_DEFAULT_ENSURE,
METHOD_WORKSPACE_DEFAULT_READ,
METHOD_WORKSPACE_ENSURE,
METHOD_WORKSPACE_ENSURE_READY,
METHOD_WORKSPACE_LIST,
METHOD_WORKSPACE_PROJECTS_ROOT_READ,
@@ -593,6 +594,8 @@ import {
type WechatRuntimeModelSetParams,
type WechatRuntimeModelSetResponse,
type WorkspaceEnsureParams,
type WorkspaceEnsureProjectParams,
type WorkspaceEnsureProjectResponse,
type WorkspaceEnsureReadyResponse,
type WorkspaceListResponse,
type WorkspacePathReadParams,
@@ -979,6 +982,10 @@ export class AppServerClient {
return this.request(METHOD_WORKSPACE_READ, params);
}
ensureWorkspace(params: WorkspaceEnsureProjectParams): JsonRpcRequest {
return this.request(METHOD_WORKSPACE_ENSURE, params);
}
readWorkspaceByPath(params: WorkspacePathReadParams): JsonRpcRequest {
return this.request(METHOD_WORKSPACE_BY_PATH_READ, params);
}
@@ -2254,6 +2261,17 @@ export class AppServerConnection {
);
}
async ensureWorkspace(
params: WorkspaceEnsureProjectParams,
options: AppServerRequestOptions = {},
): Promise<AppServerRequestResult<WorkspaceEnsureProjectResponse>> {
return await this.request<WorkspaceEnsureProjectResponse>(
this.client.ensureWorkspace(params),
METHOD_WORKSPACE_ENSURE,
options,
);
}
async readWorkspaceByPath(
params: WorkspacePathReadParams,
options: AppServerRequestOptions = {},
@@ -60,6 +60,7 @@ export const METHOD_SESSION_FILE_DELETE = "sessionFile/delete";
export const METHOD_SESSION_FILE_LIST = "sessionFile/list";
export const METHOD_WORKSPACE_LIST = "workspace/list";
export const METHOD_WORKSPACE_READ = "workspace/read";
export const METHOD_WORKSPACE_ENSURE = "workspace/ensure";
export const METHOD_WORKSPACE_BY_PATH_READ = "workspace/byPath/read";
export const METHOD_WORKSPACE_DEFAULT_READ = "workspace/default/read";
export const METHOD_WORKSPACE_DEFAULT_ENSURE = "workspace/default/ensure";
@@ -359,6 +360,7 @@ export const APP_SERVER_METHODS = [
{ method: METHOD_SESSION_FILE_LIST, kind: "request" },
{ method: METHOD_WORKSPACE_LIST, kind: "request" },
{ method: METHOD_WORKSPACE_READ, kind: "request" },
{ method: METHOD_WORKSPACE_ENSURE, kind: "request" },
{ method: METHOD_WORKSPACE_BY_PATH_READ, kind: "request" },
{ method: METHOD_WORKSPACE_DEFAULT_READ, kind: "request" },
{ method: METHOD_WORKSPACE_DEFAULT_ENSURE, kind: "request" },
@@ -1446,6 +1448,12 @@ export type WorkspacePathReadParams = {
rootPath: string;
};
export type WorkspaceEnsureProjectParams = {
name: string;
rootPath: string;
workspaceType?: string;
};
export type WorkspaceProjectPathResolveParams = {
name: string;
parentRootPath?: string;
@@ -1463,6 +1471,12 @@ export type WorkspaceReadResponse = {
workspace?: unknown;
};
export type WorkspaceEnsureProjectResponse = {
workspace: unknown;
created: boolean;
rootCreated: boolean;
};
export type WorkspaceProjectsRootReadResponse = {
rootPath: string;
};
@@ -269,6 +269,7 @@ const {
METHOD_WORKSPACE_BY_PATH_READ,
METHOD_WORKSPACE_DEFAULT_ENSURE,
METHOD_WORKSPACE_DEFAULT_READ,
METHOD_WORKSPACE_ENSURE,
METHOD_WORKSPACE_ENSURE_READY,
METHOD_WORKSPACE_LIST,
METHOD_WORKSPACE_PROJECTS_ROOT_READ,
@@ -418,6 +419,11 @@ test("builds workspace and skill read requests with current methods", () => {
const workspaceByPath = client.readWorkspaceByPath({
rootPath: "/workspace/project",
});
const ensuredWorkspace = client.ensureWorkspace({
name: "content-studio",
rootPath: "/workspace/content-studio",
workspaceType: "general",
});
const defaultWorkspace = client.readDefaultWorkspace();
const ensuredDefault = client.ensureDefaultWorkspace();
const projectsRoot = client.readWorkspaceProjectsRoot();
@@ -493,6 +499,12 @@ test("builds workspace and skill read requests with current methods", () => {
assert.deepEqual(workspaceByPath.params, {
rootPath: "/workspace/project",
});
assert.equal(ensuredWorkspace.method, METHOD_WORKSPACE_ENSURE);
assert.deepEqual(ensuredWorkspace.params, {
name: "content-studio",
rootPath: "/workspace/content-studio",
workspaceType: "general",
});
assert.equal(defaultWorkspace.method, METHOD_WORKSPACE_DEFAULT_READ);
assert.deepEqual(defaultWorkspace.params, {});
assert.equal(ensuredDefault.method, METHOD_WORKSPACE_DEFAULT_ENSURE);
@@ -1773,6 +1785,7 @@ test("exports app-server method catalog with request and notification kinds", ()
{ method: METHOD_SESSION_FILE_LIST, kind: "request" },
{ method: METHOD_WORKSPACE_LIST, kind: "request" },
{ method: METHOD_WORKSPACE_READ, kind: "request" },
{ method: METHOD_WORKSPACE_ENSURE, kind: "request" },
{ method: METHOD_WORKSPACE_BY_PATH_READ, kind: "request" },
{ method: METHOD_WORKSPACE_DEFAULT_READ, kind: "request" },
{ method: METHOD_WORKSPACE_DEFAULT_ENSURE, kind: "request" },
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@limecloud/lime-cli",
"version": "1.65.0",
"version": "1.66.0",
"description": "Lime 官方任务 CLI",
"bin": {
"lime": "scripts/run.js"
@@ -28,7 +28,6 @@ const APP_ID = "content-factory-sdk-fixture-app";
const ENTRY_KEY = "dashboard";
const ENTRY_ROUTE = "/dashboard";
const RUNTIME_VERSION = "0.8.0";
const SESSION_ID = "agent-app-electron-sdk-session";
const TASK_ID = "agent-app-electron-sdk-task-1";
const REQUEST_ID = "agent-app-electron-sdk-request-1";
const TASK_KIND = "content_factory.sdk_write";
@@ -50,6 +49,16 @@ const SDK_METHODS = [
"submitHostResponse",
"cancelTask",
];
const REQUIRED_AGENT_UI_PROJECTION_SELECTORS = [
".agent-ui-projection",
".agent-ui-main",
".agent-ui-sidecar",
".agent-message-parts",
".agent-process-timeline",
".agent-execution-graph",
".agent-artifact-refs",
".agent-evidence-refs",
];
const REQUIRED_HOST_ADAPTER_CAPABILITIES = [
"lime.agent",
"lime.storage",
@@ -64,7 +73,7 @@ Agent App Runtime Electron SDK Fixture Smoke
用途:
启动真实 Electron Desktop Host种子临时 Agent App installed state
通过正式侧栏进入 Agent App runtime page再由 iframe 内真实 SDK
通过正式侧栏 Agent Apps 聚合入口进入 Agent App runtime page再由 iframe 内真实 SDK
createLimeHostBridgeCapabilityInvoker 调用 lime.agent start/get/respond/cancel
后端使用一次性 external backend fixture 记录 App Server RuntimeCore 请求
@@ -596,7 +605,6 @@ function pageHtml() {
const APP_ID = ${JSON.stringify(APP_ID)};
const ENTRY_KEY = ${JSON.stringify(ENTRY_KEY)};
const SESSION_ID = ${JSON.stringify(SESSION_ID)};
const TASK_ID = ${JSON.stringify(TASK_ID)};
const REQUEST_ID = ${JSON.stringify(REQUEST_ID)};
const TASK_KIND = ${JSON.stringify(TASK_KIND)};
@@ -801,7 +809,6 @@ function pageHtml() {
requestId: "sdk-fixture-start-task",
args: {
taskId: TASK_ID,
sessionId: SESSION_ID,
workspaceId: "workspace-agent-app-sdk-fixture",
taskKind: TASK_KIND,
title: "Agent App SDK task fixture",
@@ -847,13 +854,20 @@ function pageHtml() {
if (!runtimeTurnId) {
throw new Error("startTask did not return runtime turnId");
}
const runtimeSessionId =
typeof startTask?.sessionId === "string" && startTask.sessionId.length > 0
? startTask.sessionId
: null;
if (!runtimeSessionId) {
throw new Error("startTask did not return runtime sessionId");
}
const firstRead = unwrap("getTask blocked", await bridge.call({
capability: "lime.agent",
method: "getTask",
requestId: "sdk-fixture-get-task-blocked",
args: {
taskId: TASK_ID,
sessionId: SESSION_ID
sessionId: runtimeSessionId
}
}));
const hostResponse = unwrap("submitHostResponse", await bridge.call({
@@ -871,7 +885,7 @@ function pageHtml() {
},
eventName: HOST_RESPONSE_EVENT_NAME,
actionScope: {
sessionId: SESSION_ID,
sessionId: runtimeSessionId,
turnId: runtimeTurnId
}
}
@@ -882,19 +896,35 @@ function pageHtml() {
requestId: "sdk-fixture-get-task-running",
args: {
taskId: TASK_ID,
sessionId: SESSION_ID,
sessionId: runtimeSessionId,
turnId: runtimeTurnId
}
}));
const artifactEvidence = readArtifactEvidenceFromTaskRecord(secondRead);
const toolEvidence = readToolCallEvidenceFromTaskRecord(secondRead);
const hostRun = unwrap("openAgentRun", await bridge.call({
capability: "lime.ui",
method: "openAgentRun",
requestId: "sdk-fixture-open-agent-run",
args: {
taskId: TASK_ID,
sessionId: runtimeSessionId,
bridgeAction: "agent_app_sdk_fixture.standard_projection",
title: "Agent App SDK Host Bridge current path",
mode: "drawer",
task: secondRead,
snapshot: secondRead,
runtimeProcess: secondRead?.runtimeProcess ?? secondRead?.process,
events: readTaskEvents(secondRead)
}
}));
const cancel = unwrap("cancelTask", await bridge.call({
capability: "lime.agent",
method: "cancelTask",
requestId: "sdk-fixture-cancel-task",
args: {
taskId: TASK_ID,
sessionId: SESSION_ID,
sessionId: runtimeSessionId,
turnId: runtimeTurnId
}
}));
@@ -910,6 +940,7 @@ function pageHtml() {
firstRead,
hostResponse,
secondRead,
hostRun,
cancel
},
artifactEvidence,
@@ -1509,23 +1540,27 @@ async function clearInvokeBuffers(page) {
});
}
async function waitForAgentAppSidebarEntry(page, options) {
async function clickAgentAppsNavEntry(page, options) {
const startedAt = Date.now();
while (Date.now() - startedAt < options.timeoutMs) {
const entry = await evaluatePageSnapshot(page, () => {
const buttons = Array.from(document.querySelectorAll("button"));
const sidebar =
document.querySelector('[data-testid="app-sidebar"]') ?? document;
const buttons = Array.from(sidebar.querySelectorAll("button"));
const matched = buttons.find((button) => {
const label = [button.getAttribute("title"), button.textContent]
const label = [
button.getAttribute("aria-label"),
button.getAttribute("title"),
button.textContent,
]
.filter(Boolean)
.join(" ");
return (
label.includes("SDK 内容工厂") ||
label.includes("content-factory-sdk-fixture-app")
);
return /Agent Apps|应用中心|App Center/i.test(label);
});
if (!matched) {
return null;
}
matched.click();
return {
title: matched.getAttribute("title"),
text: matched.textContent,
@@ -1537,24 +1572,94 @@ async function waitForAgentAppSidebarEntry(page, options) {
}
await sleep(options.intervalMs);
}
throw new Error("未在正式侧栏观察到 SDK fixture Agent App 入口");
throw new Error("未在正式侧栏观察到 Agent Apps 聚合入口");
}
async function clickAgentAppSidebarEntry(page) {
const button = page
.locator("button")
.filter({ hasText: /SDK 内容工厂|content-factory-sdk-fixture-app/ })
async function waitForAgentAppsInstalledRow(page, options) {
const rowSelector = `[data-testid="agent-apps-list-row-${APP_ID}"]`;
const installedSelector = `[data-testid="agent-apps-installed-${APP_ID}"]`;
const startedAt = Date.now();
let lastSnapshot = null;
while (Date.now() - startedAt < options.timeoutMs) {
const snapshot = await evaluatePageSnapshot(
page,
({ rowSelector, installedSelector }) => {
const pageRoot = document.querySelector(
'[data-testid="agent-apps-page"]',
);
const list = document.querySelector('[data-testid="agent-apps-list"]');
const row = document.querySelector(rowSelector);
const installedMarker = document.querySelector(installedSelector);
return {
pageVisible: Boolean(pageRoot),
listVisible: Boolean(list),
rowVisible: Boolean(row),
installedMarkerVisible: Boolean(installedMarker),
url: window.location.href,
bodyText: document.body?.innerText?.slice(0, 1_200) ?? "",
};
},
{ rowSelector, installedSelector },
);
if (!snapshot) {
await sleep(options.intervalMs);
continue;
}
lastSnapshot = snapshot;
if (
snapshot.pageVisible &&
snapshot.listVisible &&
snapshot.rowVisible &&
snapshot.installedMarkerVisible
) {
return {
...snapshot,
bodyText: sanitizeText(snapshot.bodyText),
};
}
await sleep(options.intervalMs);
}
throw new Error(
`Agent Apps 聚合页未显示 SDK fixture installed row: ${JSON.stringify(
sanitizeJson(lastSnapshot),
null,
2,
)}`,
);
}
async function openAgentAppRuntimeFromAgentAppsPage(page, options) {
const navEntry = await clickAgentAppsNavEntry(page, options);
await page.waitForSelector('[data-testid="agent-apps-page"]', {
timeout: options.timeoutMs,
});
await page.waitForSelector('[data-testid="agent-apps-list"]', {
timeout: options.timeoutMs,
});
const installedRow = await waitForAgentAppsInstalledRow(page, options);
const detailButton = page
.locator(`[data-testid="agent-apps-open-detail-${APP_ID}"]`)
.first();
if ((await button.count()) > 0) {
await button.click();
return;
}
const titled = page.locator('button[title*="SDK 内容工厂"]').first();
if ((await titled.count()) > 0) {
await titled.click();
return;
}
throw new Error("SDK fixture Agent App 侧栏按钮不可点击");
await detailButton.click({ timeout: options.timeoutMs });
await page.waitForSelector('[data-testid="agent-apps-detail"]', {
timeout: options.timeoutMs,
});
const launchEntrySelector = `[data-testid="agent-apps-launch-entry-${ENTRY_KEY}"]`;
await page.waitForSelector(launchEntrySelector, {
timeout: options.timeoutMs,
});
await page.locator(launchEntrySelector).first().click({
timeout: options.timeoutMs,
});
return {
navEntry,
installedRow,
detailVisible:
(await page.locator('[data-testid="agent-apps-detail"]').count()) > 0,
launchEntryKey: ENTRY_KEY,
};
}
async function waitForSdkFixtureResult(page, options) {
@@ -1617,6 +1722,97 @@ async function captureRuntimePageEvidence(page) {
});
}
async function waitForHostProjectionSurface(page, options) {
const startedAt = Date.now();
let lastSnapshot = null;
while (Date.now() - startedAt < options.timeoutMs) {
const snapshot = await evaluatePageSnapshot(
page,
(requiredSelectors) => {
const dock = document.querySelector(
'[data-testid="agent-app-host-agent-run-dock"]',
);
const drawer = document.querySelector(
'[data-testid="agent-app-host-agent-run-drawer"]',
);
const missingSelectors = requiredSelectors.filter(
(selector) => !document.querySelector(selector),
);
const projectionRoot = document.querySelector(".agent-ui-projection");
const action = document.querySelector("[data-action-id]");
const actionRequiredList = document.querySelector(
".agent-action-required-list",
);
const artifact = document.querySelector(
'.agent-artifact-refs [data-ref-kind="artifact"]',
);
const evidence = document.querySelector(
'.agent-evidence-refs [data-ref-kind="evidence"]',
);
const toolEntry = document.querySelector(
'.agent-process-entry[data-entry-kind="tool"]',
);
return {
dockVisible: Boolean(dock),
drawerVisible: Boolean(drawer),
projectionVisible: Boolean(projectionRoot),
runtimeStatus:
projectionRoot instanceof HTMLElement
? projectionRoot.dataset.runtimeStatus ?? null
: null,
hydrationStatus:
projectionRoot instanceof HTMLElement
? projectionRoot.dataset.hydrationStatus ?? null
: null,
missingSelectors,
actionId:
action instanceof HTMLElement ? action.dataset.actionId ?? null : null,
actionRequiredListVisible: Boolean(actionRequiredList),
artifactRefId:
artifact instanceof HTMLElement ? artifact.dataset.refId ?? null : null,
evidenceRefId:
evidence instanceof HTMLElement ? evidence.dataset.refId ?? null : null,
toolEntryVisible: Boolean(toolEntry),
textPreview: document.body?.innerText?.slice(0, 1_000) ?? "",
};
},
REQUIRED_AGENT_UI_PROJECTION_SELECTORS,
);
if (!snapshot) {
await sleep(options.intervalMs);
continue;
}
lastSnapshot = snapshot;
if (snapshot.dockVisible && !snapshot.drawerVisible) {
await page
.locator('[data-testid="agent-app-host-agent-run-dock"]')
.first()
.click({ timeout: Math.min(options.intervalMs, 1_000) })
.catch(() => {});
await sleep(options.intervalMs);
continue;
}
if (
snapshot.drawerVisible &&
snapshot.projectionVisible &&
snapshot.missingSelectors.length === 0
) {
return {
...snapshot,
textPreview: sanitizeText(snapshot.textPreview),
};
}
await sleep(options.intervalMs);
}
throw new Error(
`Host Agent Run 标准 AgentUI projection surface 未就绪: ${JSON.stringify(
sanitizeJson(lastSnapshot),
null,
2,
)}`,
);
}
async function stopRuntimeFromPage(page) {
return await page.evaluate(
async ({ appId }) => {
@@ -1663,8 +1859,9 @@ function assertSdkLifecycleResult(result, backendSummary) {
"SDK startTask taskId 不正确",
);
assert(
result.taskLifecycle?.startTask?.sessionId === SESSION_ID,
"SDK startTask sessionId 不正确",
typeof result.taskLifecycle?.startTask?.sessionId === "string" &&
result.taskLifecycle.startTask.sessionId.length > 0,
"SDK startTask 未返回有效 sessionId",
);
assert(
typeof result.taskLifecycle?.startTask?.turnId === "string" &&
@@ -1675,6 +1872,10 @@ function assertSdkLifecycleResult(result, backendSummary) {
result.ids?.turnId === result.taskLifecycle?.startTask?.turnId,
"SDK ids.turnId 未使用 startTask 返回的 runtime turnId",
);
assert(
result.ids?.sessionId === result.taskLifecycle?.startTask?.sessionId,
"SDK ids.sessionId 未使用 startTask 返回的 runtime sessionId",
);
assert(
result.taskLifecycle?.firstRead?.status === "running",
`首次 SDK getTask status 应为 running,实际 ${result.taskLifecycle?.firstRead?.status}`,
@@ -1687,6 +1888,14 @@ function assertSdkLifecycleResult(result, backendSummary) {
result.taskLifecycle?.secondRead?.status === "running",
`submitHostResponse 后 SDK getTask status 应为 running,实际 ${result.taskLifecycle?.secondRead?.status}`,
);
assert(
result.taskLifecycle?.hostRun?.surface === "host_agent_run",
"SDK lime.ui.openAgentRun 未返回 host_agent_run surface",
);
assert(
result.taskLifecycle?.hostRun?.opened === true,
"SDK lime.ui.openAgentRun 未打开 Host Agent Run 面板",
);
assert(
result.taskLifecycle?.cancel?.status === "cancelled",
`SDK cancelTask status 应为 cancelled,实际 ${result.taskLifecycle?.cancel?.status}`,
@@ -1772,8 +1981,8 @@ function assertSdkLifecycleResult(result, backendSummary) {
"turn_config provider_config.model 未抵达 backend",
);
assert(
backendSummary.startSessionId === SESSION_ID,
"external backend turnStart sessionId 不正确",
backendSummary.startSessionId === result.taskLifecycle?.startTask?.sessionId,
"external backend turnStart sessionId 未与 SDK startTask 返回值一致",
);
assert(
backendSummary.startTurnId === result.taskLifecycle?.startTask?.turnId,
@@ -1839,7 +2048,7 @@ async function run() {
checkedAt: new Date().toISOString(),
appId: APP_ID,
entryKey: ENTRY_KEY,
sessionId: SESSION_ID,
sessionId: null,
taskId: TASK_ID,
turnId: null,
requestId: REQUEST_ID,
@@ -1856,6 +2065,9 @@ async function run() {
backendLogPath: options.keepTemp ? runtimeEnv.backendLogPath : null,
electronPreloadBridge: false,
sidebarEntryVisible: false,
agentAppsPageVisible: false,
agentAppsInstalledRowVisible: false,
agentAppsLaunchEntryVisible: false,
runtimeSurfaceVisible: false,
runtimeFrameVisible: false,
frameSrc: null,
@@ -1915,17 +2127,26 @@ async function run() {
rendererSnapshot.electron && rendererSnapshot.hasInvokeBridge;
await clearInvokeBuffers(page);
logStage("wait-sidebar-entry");
const sidebarEntry = await waitForAgentAppSidebarEntry(page, options);
logStage("open-agent-apps-runtime-page");
const agentAppsLaunch = await openAgentAppRuntimeFromAgentAppsPage(
page,
options,
);
summary.sidebarEntryVisible = true;
summary.sidebarEntry = sanitizeJson(sidebarEntry);
logStage("open-runtime-page");
await clickAgentAppSidebarEntry(page);
summary.sidebarEntry = sanitizeJson(agentAppsLaunch.navEntry);
summary.agentAppsPageVisible = true;
summary.agentAppsInstalledRowVisible = true;
summary.agentAppsLaunchEntryVisible = true;
summary.agentAppsLaunch = sanitizeJson(agentAppsLaunch);
logStage("wait-sdk-result");
const sdkEvidence = await waitForSdkFixtureResult(page, options);
const runtimeEvidence = await captureRuntimePageEvidence(page);
logStage("wait-host-agentui-projection");
const hostProjectionEvidence = await waitForHostProjectionSurface(
page,
options,
);
const traceEntries = invokeTraceEntriesFromStorage(
runtimeEvidence?.traceRaw,
);
@@ -1934,6 +2155,7 @@ async function run() {
const backendSummary = summarizeBackendLog(backendEntries);
Object.assign(summary, {
sessionId: sdkEvidence.result?.taskLifecycle?.startTask?.sessionId ?? null,
turnId: sdkEvidence.result?.taskLifecycle?.startTask?.turnId ?? null,
runtimeSurfaceVisible: Boolean(runtimeEvidence?.surfaceVisible),
runtimeFrameVisible: Boolean(runtimeEvidence?.frameVisible),
@@ -1943,6 +2165,7 @@ async function run() {
sdkEvidence.result?.artifactEvidence ?? null,
),
toolEvidence: sanitizeJson(sdkEvidence.result?.toolEvidence ?? null),
hostProjectionEvidence: sanitizeJson(hostProjectionEvidence),
frameContent: sanitizeJson({
url: sdkEvidence.url,
bodyTextPreview: sdkEvidence.bodyTextPreview,
@@ -2005,6 +2228,22 @@ async function run() {
summary.appServerHandleJsonLinesSeen,
"未观察到 app_server_handle_json_lines",
);
assert(
summary.hostProjectionEvidence?.projectionVisible === true,
"未观察到 Host Agent Run 标准 AgentUI projection surface",
);
assert(
summary.hostProjectionEvidence?.artifactRefId,
"Host Agent Run 标准 ArtifactRef surface 未渲染",
);
assert(
summary.hostProjectionEvidence?.evidenceRefId,
"Host Agent Run 标准 EvidenceRef surface 未渲染",
);
assert(
summary.hostProjectionEvidence?.toolEntryVisible === true,
"Host Agent Run 标准 ProcessTimeline 未渲染 tool entry",
);
assert(
consoleErrors.length === 0,
`观察到 console error: ${consoleErrors.join(" | ")}`,
@@ -22,8 +22,13 @@ describe("agent app runtime Electron SDK fixture smoke guard", () => {
expect(content).toContain(
'typeof window.electronAPI?.invoke === "function"',
);
expect(content).toContain("waitForAgentAppSidebarEntry(");
expect(content).toContain("clickAgentAppSidebarEntry(");
expect(content).toContain("clickAgentAppsNavEntry(");
expect(content).toContain("openAgentAppRuntimeFromAgentAppsPage(");
expect(content).toContain('[data-testid="agent-apps-page"]');
expect(content).toContain('[data-testid="agent-apps-list"]');
expect(content).toContain("agent-apps-list-row-${APP_ID}");
expect(content).toContain("agent-apps-open-detail-${APP_ID}");
expect(content).toContain("agent-apps-launch-entry-${ENTRY_KEY}");
expect(content).toContain('[data-testid="agent-app-runtime-surface"]');
expect(content).toContain('[data-testid="agent-app-runtime-frame"]');
expect(content).toContain("__agentAppSdkFixtureResult");
@@ -71,14 +76,22 @@ describe("agent app runtime Electron SDK fixture smoke guard", () => {
expect(content).not.toContain("?? TURN_ID");
expect(content).not.toContain("agent-app-electron-sdk-turn-1");
expect(content).toContain("const runtimeTurnId =");
expect(content).toContain("const runtimeSessionId =");
expect(content).toContain("startTask did not return runtime turnId");
expect(content).toContain("startTask did not return runtime sessionId");
expect(content).toMatch(
/method: "submitHostResponse"[\s\S]*actionScope: \{[\s\S]*sessionId: SESSION_ID,[\s\S]*turnId: runtimeTurnId[\s\S]*\}/,
/method: "submitHostResponse"[\s\S]*actionScope: \{[\s\S]*sessionId: runtimeSessionId,[\s\S]*turnId: runtimeTurnId[\s\S]*\}/,
);
expect(content).toMatch(
/method: "cancelTask"[\s\S]*args: \{[\s\S]*taskId: TASK_ID,[\s\S]*sessionId: SESSION_ID,[\s\S]*turnId: runtimeTurnId[\s\S]*\}/,
/method: "cancelTask"[\s\S]*args: \{[\s\S]*taskId: TASK_ID,[\s\S]*sessionId: runtimeSessionId,[\s\S]*turnId: runtimeTurnId[\s\S]*\}/,
);
expect(content).not.toContain("const SESSION_ID");
expect(content).not.toContain("sessionId: SESSION_ID");
expect(content).toContain("SDK startTask 未返回有效 turnId");
expect(content).toContain("SDK startTask 未返回有效 sessionId");
expect(content).toContain(
"external backend turnStart sessionId 未与 SDK startTask 返回值一致",
);
expect(content).toContain(
"external backend turnStart turnId 未与 SDK startTask 返回值一致",
);
@@ -88,6 +101,9 @@ describe("agent app runtime Electron SDK fixture smoke guard", () => {
expect(content).toContain(
"turnId: sdkEvidence.result?.taskLifecycle?.startTask?.turnId ?? null",
);
expect(content).toContain(
"sessionId: sdkEvidence.result?.taskLifecycle?.startTask?.sessionId ?? null",
);
expect(content).not.toContain("backendSummary.startTurnId === TURN_ID");
expect(content).not.toContain("backendSummary.cancelTurnId === TURN_ID");
});
@@ -118,6 +134,29 @@ describe("agent app runtime Electron SDK fixture smoke guard", () => {
);
});
it("proves the Host Agent Run standard projection without requiring stale pending actions", () => {
const content = readSmokeScript();
const selectorDeclaration = content.match(
/const REQUIRED_AGENT_UI_PROJECTION_SELECTORS = \[[\s\S]*?\];/,
)?.[0] ?? "";
expect(selectorDeclaration).toContain('".agent-ui-projection"');
expect(selectorDeclaration).toContain('".agent-ui-main"');
expect(selectorDeclaration).toContain('".agent-ui-sidecar"');
expect(selectorDeclaration).toContain('".agent-message-parts"');
expect(selectorDeclaration).toContain('".agent-process-timeline"');
expect(selectorDeclaration).toContain('".agent-execution-graph"');
expect(selectorDeclaration).toContain('".agent-artifact-refs"');
expect(selectorDeclaration).toContain('".agent-evidence-refs"');
expect(selectorDeclaration).not.toContain(".agent-action-required-list");
expect(content).toContain("actionRequiredListVisible");
expect(content).toContain("Host Agent Run 标准 EvidenceRef surface 未渲染");
expect(content).toContain("Host Agent Run 标准 ProcessTimeline 未渲染 tool entry");
expect(content).not.toContain(
"Host Agent Run 标准 ActionRequired surface 未关联 fixture requestId",
);
});
it("replays App Server tool call facts into the SDK task record", () => {
const content = readSmokeScript();
@@ -2182,12 +2182,12 @@ function buildRuntimeSummaryCheckScript() {
text.includes("拒绝并回滚"),
hasWebSearchReady: text.includes("WebSearch 已接通"),
hasSubagentReady: text.includes("子任务核心 tools 已接通"),
hasTeamReady: text.includes("Team current tools 已接通"),
hasTeamReady: text.includes("Subagents 协作 tools 已接通"),
hasTaskReady: text.includes("Task current tools 已接通"),
hasReadyBanner: text.includes("当前 runtime current surface 已覆盖 WebSearch、子任务、Team 与 Task 主链。"),
hasReadyBanner: text.includes("当前 runtime current surface 已覆盖 WebSearch、子任务、Subagents 协作与 Task 主链。"),
hasWebSearchGap: text.includes("WebSearch 未接通"),
hasSubagentGap: text.includes("子任务核心 tools 缺"),
hasTeamGap: text.includes("Team current tools 缺"),
hasTeamGap: text.includes("Subagents 协作 tools 缺"),
hasTaskGap: text.includes("Task current tools 缺"),
hasGapBanner: text.includes("当前 runtime current surface 仍有缺口"),
hasLegacyWebSearchWarning: text.includes(${JSON.stringify(FORBIDDEN_PAGE_WARNINGS[0])}),
+174 -7
View File
@@ -1241,6 +1241,7 @@ const checks = [
"METHOD_WORKSPACE_LIST",
"METHOD_WORKSPACE_READ",
"METHOD_WORKSPACE_BY_PATH_READ",
"METHOD_WORKSPACE_ENSURE",
"METHOD_WORKSPACE_DEFAULT_READ",
"METHOD_WORKSPACE_DEFAULT_ENSURE",
"METHOD_WORKSPACE_PROJECTS_ROOT_READ",
@@ -1255,13 +1256,16 @@ const checks = [
"METHOD_WORKSPACE_PROJECT_PATH_RESOLVE",
"METHOD_WORKSPACE_LIST",
"METHOD_WORKSPACE_DEFAULT_READ",
"METHOD_WORKSPACE_ENSURE",
"METHOD_WORKSPACE_ENSURE_READY",
"METHOD_WORKSPACE_DEFAULT_ENSURE",
"METHOD_WORKSPACE_BY_PATH_READ",
"METHOD_WORKSPACE_READ",
"workspace id is required to ensure App Server workspace",
"workspace rootPath is required to ensure project",
"App Server workspace/projectsRoot/read did not return rootPath",
"App Server workspace/projectPath/resolve did not return rootPath",
"App Server workspace/ensure did not return workspace",
"App Server workspace/ensureReady did not return result",
"App Server workspace/default/ensure did not return workspace",
],
@@ -1286,6 +1290,7 @@ const checks = [
"应该通过 App Server 获取 workspace 根目录",
"应该通过 App Server 解析项目目录",
"应该通过 App Server 按路径获取项目",
"应该通过 App Server ensure 本地项目工作区",
"应该通过 App Server 获取并标准化默认项目",
"应该通过 App Server 确保工作区目录就绪",
"确保默认工作区目录就绪时应先 ensure 默认 workspace 再 ensureReady",
@@ -1294,6 +1299,7 @@ const checks = [
'"workspace/projectsRoot/read"',
'"workspace/projectPath/resolve"',
'"workspace/byPath/read"',
'"workspace/ensure"',
'"workspace/default/read"',
'"workspace/default/ensure"',
'"workspace/ensureReady"',
@@ -2284,7 +2290,7 @@ const checks = [
"toolEvidence: sanitizeJson(sdkEvidence.result?.toolEvidence ?? null)",
"SDK secondRead 未从 App Server read model replay task:toolCall",
"SDK secondRead result thread_read.tool_calls missing App Server tool call",
"external backend turnStart sessionId 不正确",
"external backend turnStart sessionId 未与 SDK startTask 返回值一致",
"external backend actionRespond requestId 不正确",
"external backend turnCancel turnId 未使用 SDK startTask 返回值",
],
@@ -4232,11 +4238,11 @@ const checks = [
file: "src/components/agent/chat/projection/agentUiProjectionSummary.ts",
snippets: [
'from "@limecloud/agent-runtime-projection"',
"summarizeAgentUiTeamWorkbenchSurfaceLanesBase",
"summarizeAgentUiTeamWorkbenchSurfacesBase",
"summarizeAgentUiSubagentsSurfaceLanesBase",
"summarizeAgentUiSubagentsSurfacesBase",
"findLatestAgentUiProjectionEventForArtifact",
"summarizeAgentUiProjectionEvents",
"summarizeAgentUiTeamWorkbenchProjectionEvents",
"summarizeAgentUiSubagentsProjectionEvents",
],
absentSnippets: [
"export const AGENT_UI_ACTION_EVENT_TYPES = new Set",
@@ -4244,10 +4250,10 @@ const checks = [
"export const AGENT_UI_ARTIFACT_EVENT_TYPES = new Set",
"export const AGENT_UI_DIAGNOSTIC_EVENT_TYPES = new Set",
"export const AGENT_UI_EVIDENCE_EVENT_TYPES = new Set",
"export const AGENT_UI_TEAM_WORKBENCH_SURFACES = new Set",
"export const AGENT_UI_SUBAGENTS_SURFACES = new Set",
"export const AGENT_UI_NOTABLE_EVENT_TYPES = new Set",
"export function summarizeAgentUiProjectionEvents(",
"export function summarizeAgentUiTeamWorkbenchProjectionEvents(",
"export function summarizeAgentUiSubagentsProjectionEvents(",
"export function findLatestAgentUiProjectionEventForArtifact(",
"function normalizeLookupKey",
],
@@ -5157,7 +5163,8 @@ const checks = [
'import { projectAgentUiState } from "@limecloud/agent-runtime-projection"',
"buildAgentAppStandardRuntimeEvents",
"return projectAgentUiState({ executionEvents })",
"buildAgentRunStandardProjectionStateFromState(run)",
"buildSharedProjectionInput(run, t)",
"buildAgentRunStandardProjectionStateFromState(sharedProjectionInput)",
"standardState: AgentUiProjectionState",
'data-testid="agent-run-standard-projection"',
"<AgentUiProjectionView",
@@ -5511,6 +5518,166 @@ const checks = [
"invokeMockOnly",
],
},
{
name: "Agent App runtime standard client adapter uses App Server AgentRuntimeClient current methods",
files: [
"src/features/agent-app/runtime/agentRuntimeCapabilityApi.ts",
"src/features/agent-app/runtime/agentRuntimeClientApi.ts",
"src/features/agent-app/runtime/agentRuntimeClientApi.test.ts",
],
snippets: [
'@limecloud/agent-runtime-client',
"createFailClosedAgentAppRuntimeCapabilityApi",
"createAgentAppRuntimeCapabilityApiFromClient",
'"startTurn" | "readThread" | "cancelTurn" | "respondAction"',
"runtimeClient.startTurn(startParams)",
"runtimeClient.readThread({",
"runtimeClient.cancelTurn({",
"runtimeClient.respondAction(",
"AgentRuntimeClient adapter requires an existing sessionId",
"hostOptions: {",
"asterChatRequest",
"turn_config: turnConfig",
"threadReadFromAgentSessionRead(response)",
"detail?.thread_read",
"detail?.threadRead",
"requires a standard AgentRuntimeClient or explicit compat api",
"把 Agent App startTask 投影到标准 AgentRuntimeClient.startTurn",
"没有 sessionId 时 fail closed,不伪造独立 task 协议",
],
absentSnippets: [
"defaultAgentAppRuntimeCapabilityApi",
"safeInvoke",
"agent_app_runtime_start_task",
"agent_app_runtime_get_task",
"agent_app_runtime_cancel_task",
"agent_app_runtime_submit_host_response",
"agent_runtime_submit_turn",
"agent_runtime_get_thread_read",
"agent_runtime_interrupt_turn",
"agent_runtime_respond_action",
"mockPriorityCommands",
"defaultMocks",
"invokeMockOnly",
],
},
{
name: "Agent runtime client package exposes session gateway adapter",
files: [
"packages/agent-runtime-client/package.json",
"packages/agent-runtime-client/README.md",
"packages/agent-runtime-client/src/index.ts",
"packages/agent-runtime-client/src/sessionGateway.ts",
"packages/agent-runtime-client/tests/client.test.mjs",
"tsconfig.json",
"vite.config.ts",
],
snippets: [
'"./sessionGateway"',
"@limecloud/agent-runtime-client/sessionGateway",
"./packages/agent-runtime-client/src/sessionGateway.ts",
"browser-safe 子路径",
"createAgentRuntimeClientFromSessionGateway",
"type AgentRuntimeLifecycleClient",
"type AgentRuntimeSessionGateway",
'"startTurn" | "readThread" | "cancelTurn" | "respondAction"',
"readSession(",
"callAgentRuntimeSessionGateway(gateway.readSession, params, options)",
"if (options === undefined)",
"createAgentRuntimeClientFromSessionGateway adapts an existing session gateway",
"readSession",
"timeoutMs: 120_000",
],
absentSnippets: [
"safeInvoke",
"mockPriorityCommands",
"defaultMocks",
"invokeMockOnly",
"agent_app_runtime_",
"agent_runtime_submit_turn",
"fetch(",
"XMLHttpRequest",
],
},
{
name: "Agent App capability host accepts standard AgentRuntimeClient injection",
files: [
"src/features/agent-app/types.ts",
"src/features/agent-app/types.d.ts",
"src/features/agent-app/runtime/agentRuntimeCapabilityApi.ts",
"src/features/agent-app/runtime/agentRuntimeCapabilityHost.ts",
"src/features/agent-app/runtime/agentRuntimeCapabilityHost.test.ts",
],
snippets: [
'import type { AgentRuntimeClient } from "@limecloud/agent-runtime-client"',
"runtimeClient?: Pick<",
'"startTurn" | "readThread" | "cancelTurn" | "respondAction"',
"createAgentAppRuntimeCapabilityApiFromClient(options.runtimeClient",
"createFailClosedAgentAppRuntimeCapabilityApi()",
"requires a standard AgentRuntimeClient or explicit compat api",
"sessionId?: string;",
"workspaceId?: string;",
"没有标准 runtime client 或显式 compat api 时不再隐式回退旧 facade",
"可以直接注入标准 AgentRuntimeClient 驱动 lime.agent task",
'sessionId: "session-standard"',
"provider_config: {",
'model_name: "claude-sonnet-4"',
"artifact:created",
],
absentSnippets: [
"defaultAgentAppRuntimeCapabilityApi",
"AGENT_RUNTIME_COMMANDS.submitTurn",
"agent_runtime_submit_turn",
"agent_runtime_get_thread_read",
"mockPriorityCommands",
"defaultMocks",
"invokeMockOnly",
],
},
{
name: "Agent App runtime page defaults to App Server backed standard runtime client",
files: [
"src/features/agent-app/runtime/agentRuntimeAppServerClient.ts",
"src/features/agent-app/runtime/agentRuntimeAppServerClient.test.ts",
"src/features/agent-app/ui/AgentAppRuntimePage.tsx",
"src/features/agent-app/ui/AgentAppRuntimePage.hostBridge.test.tsx",
"src/features/agent-app/ui/AgentAppRuntimePage.testFixtures.tsx",
],
snippets: [
"@limecloud/agent-runtime-client/sessionGateway",
"createAgentRuntimeClientFromSessionGateway",
"type AgentRuntimeLifecycleClient",
"type AgentRuntimeSessionGateway",
"createAgentAppRuntimeClientFromAppServer",
"createAgentAppRuntimeSessionResolver",
"createDefaultAgentAppRuntimeHostOptions",
"appServerClient.startSession({",
'kind: "agent_app.task"',
"createAgentRuntimeClientFromSessionGateway as",
"runtimeClient: createAgentAppRuntimeClientFromAppServer(appServerClient)",
"ensureSession: createAgentAppRuntimeSessionResolver(appServerClient)",
"...createDefaultAgentAppRuntimeHostOptions()",
"app_server_agent_runtime_client",
"appServerClientMocks.startSession",
"appServerClientMocks.startTurn",
"appServerClientMocks.readSession",
"appServerClientMocks.respondAction",
"runtimeApiMocks.startAgentAppRuntimeTask).not.toHaveBeenCalled()",
"runtimeApiMocks.getAgentAppRuntimeTask).not.toHaveBeenCalled()",
"通过 agentSession/start 为 Agent App 默认宿主创建 current session",
"默认 Host options 同时提供 runtime client 与 session resolver,且 session 失败时 fail closed",
],
absentSnippets: [
"safeInvoke",
"agent_app_runtime_start_task",
"agent_app_runtime_get_task",
"agent_app_runtime_cancel_task",
"agent_app_runtime_submit_host_response",
"mockPriorityCommands",
"defaultMocks",
"invokeMockOnly",
],
},
{
name: "Agent App runtime task record replays App Server read model tool calls",
files: [
+41
View File
@@ -769,6 +769,7 @@ const currentElectronHostRequiredCommands = new Set([
"project_memory_get",
"save_experimental_config",
"start_oem_cloud_oauth_callback_bridge",
"workspace_ensure",
"workspace_ensure_default_ready",
"workspace_ensure_ready",
"workspace_get",
@@ -4638,6 +4639,7 @@ function main() {
const registeredCommands = collectElectronHostCommands();
const mockPriorityCommands = collectMockPriorityCommands();
const bridgeTruthCommands = collectBridgeTruthCommands();
const noMockFallbackCompatCommands = collectNoMockFallbackCompatCommands();
const agentCommandCatalog = readAgentCommandCatalog();
const retiredMemoryRuntimeCatalogSectionLeaks = Object.keys(
agentCommandCatalog,
@@ -4755,6 +4757,21 @@ function main() {
(command) => !bridgeTruthCommands.has(command),
),
);
const agentAppRuntimeDevBridgeTruthLeaks = new Set(
[...currentAgentAppRuntimeDesktopHostCommands].filter((command) =>
bridgeTruthCommands.has(command),
),
);
const missingAgentAppRuntimeNoMockCompatCommands = new Set(
[...currentAgentAppRuntimeDesktopHostCommands].filter(
(command) => !noMockFallbackCompatCommands.has(command),
),
);
const agentAppRuntimeMockPriorityLeaks = new Set(
[...currentAgentAppRuntimeDesktopHostCommands].filter((command) =>
mockPriorityCommands.has(command),
),
);
const deprecatedCommandsStillUsed = new Set(
[...frontendCommands].filter((command) => deprecatedCommands.has(command)),
);
@@ -5322,6 +5339,30 @@ function main() {
);
}
if (agentAppRuntimeDevBridgeTruthLeaks.size > 0) {
hasError = true;
printCommandGroup(
"Agent App runtime task compat 命令不能回到 DevBridge truth surface",
agentAppRuntimeDevBridgeTruthLeaks,
);
}
if (missingAgentAppRuntimeNoMockCompatCommands.size > 0) {
hasError = true;
printCommandGroup(
"Agent App runtime task compat 命令必须保留 no-mock fail-closed 分类",
missingAgentAppRuntimeNoMockCompatCommands,
);
}
if (agentAppRuntimeMockPriorityLeaks.size > 0) {
hasError = true;
printCommandGroup(
"Agent App runtime task compat 命令不能回到 mock priority surface",
agentAppRuntimeMockPriorityLeaks,
);
}
if (productionBridgeGuardFailures.length > 0) {
hasError = true;
printGuardFailures(

Some files were not shown because too many files have changed in this diff Show More